JSON Toolkit

Вопросы о запросах и валидации: JSONPath и Schema

Главное: Использовать для запросов по пути JSONPath или Pointer, как выбрать между Patch и Merge Patch, что способна блокировать Schema и как выбрать версию драфта — всё объясняется за один проход.

Средний~12 мин чтенияОпубликовано 2026-10-30Обновлено 2026-10-30
Обложка вопросов о запросах и валидации

Главное: используйте JSONPath, чтобы находить узлы по условию, JSON Pointer или JSON Patch — чтобы адресовать и редактировать точные места, а Merge Patch — чтобы слить конфигурацию; привязывайте новые Schema к 2020-12 и включайте allErrors, чтобы все ошибки возвращались за один проход.

Запрос — это «найти»; валидация — это «подтвердить правильность». У каждого свой стандарт, поэтому не смешивайте их.

1. Запросы по путям

Как выбрать между JSONPath и JSON Pointer?

Зависит от того, «находите ли вы по условию» или «получаете по позиции».

Аспект JSONPath JSON Pointer
Стандарт RFC 9535 (2024) RFC 6901 (2013)
Выражение $.users[?(@.age > 18)].id /users/0/id
Подстановочные знаки и рекурсия Поддерживает *, .. Не поддерживает
Условная фильтрация Поддерживает Не поддерживает
Результат Набор узлов (возможно, несколько) Единое место
Выразительность Сильная, но с тяжёлым наследием различий реализаций Слабая, но полностью детерминированная
Типичное применение Извлечение из логов, отладка, исследование данных JSON Patch, адресация конфигурации, пути ошибок

Правило выбора простое:

  • Нужно «получить все узлы, соответствующие условию» → JSONPath;
  • Нужно «закрепить одно точное место и добавить, удалить или изменить его» → JSON Pointer;
  • Использовать вместе: находить узел через JSONPath и описывать его место через Pointer.

Не используйте JSONPath для адресации в контракте API. JSONPath возвращает набор, а три ветки для пустого, одноэлементного и многоэлементного результатов усложняют протокол; Pointer означает «либо указывает на существующее место, либо не существует», что лучше подходит для контракта.

В чём разница между $.. и $.*?

$.. — это рекурсивный спуск, обходящий каждый уровень; $.* возвращает только непосредственных потомков текущего уровня.

{
  "a": { "id": 1, "child": { "id": 2 } },
  "b": { "id": 3 }
}
  • $.* → два объекта {id:1, child:{...}} и {id:3};
  • $..id → три значения: 1, 2, 3;
  • $.*.id → 1 и 3, исключая вложенный 2.

Рекурсивный спуск удобен, но дорог: $.. обходит всё дерево, что на большом документе даёт O(n), и легко захватывает глубокие поля, которые вы не имели в виду. Если конкретный уровень работает, избегайте ...

Могут ли фильтры сравнивать строки? Могут ли они сопоставлять через регулярное выражение?

Сравнение строк работает; регулярное выражение зависит от реализации.

Стандартный синтаксис (RFC 9535):

$.items[?(@.status == "active")]
$.items[?(@.price > 10 && @.stock > 0)]
$.items[?(@.name == $.target)]      // сравнивать с другим узлом

Сравнение строк использует двойные кавычки (одинарные кавычки тоже разрешены в RFC 9535, но некоторые старые реализации принимают только двойные). Числовое сравнение следует числовой семантике; смешивание строки с числом, например "10" > 9, в стандарте — неопределённое поведение, поэтому не полагайтесь на него.

Что касается регулярных выражений, RFC 9535 определяет функции match() и search(), но поддержка которых существенно различается между реализациями. Безопасный подход: пусть JSONPath делает грубую фильтрацию, а сопоставление по регулярному выражению выполняйте в коде после получения результатов.

Использовать JSON Patch или Merge Patch?

Оба описывают «как что-то изменить», но их выразительность и семантика совершенно различны.

Аспект JSON Patch (RFC 6902) Merge Patch (RFC 7386)
Форма Массив операций [{"op":"add",...}] Частичный объект
Поддерживаемые операции add / remove / replace / move / copy / test Только слияние (удаление через null)
Может адресовать элементы массива Да, по индексу Нет, весь массив заменяется
Атомарность test обеспечивает оптимистичную блокировку Отсутствует
Размер документа Большой, но точный Малый, но ограничен по выразительности
Подходит для Мелкозернистых правок, частичных обновлений API Слияния конфигурации, частичной отправки формы

Критерии выбора:

  • Изменить поле третьего элемента в массиве → JSON Patch;
  • Слить несколько записей конфигурации в существующую конфигурацию → Merge Patch;
  • Получить контроль параллелизма «ошибка, если исходное значение не X» → test в JSON Patch.

Обратите внимание на ловушку семантики null в Merge Patch: null означает «удалить этот ключ», а не «установить значение в null». Установить значение в null через Merge Patch невозможно.

2. Валидация Schema

Какие проблемы решает JSON Schema?

Она превращает «как должны выглядеть данные» из словесной договорённости в проверяемый машиной контракт. Ошибки, которые она реально способна блокировать:

  • Отсутствующие обязательные поля (required);
  • Неверный тип, например строка там, где ожидается число (type);
  • Значения вне диапазона (minimum / maximum / minLength);
  • Значения вне допустимого набора (enum);
  • Несогласованные типы элементов массива или неверная длина (items / minItems);
  • Отсутствующие поля во вложенных структурах (рекурсивная валидация через properties);
  • Лишние поля (additionalProperties: false).

Что она не решает: бизнес-правила («время окончания должно быть позже времени начала»), межполевые условия (некоторые можно выразить через if/then, но читаемость страдает), уникальность в базе данных и проверки прав.

Итак, Schema — привратник «формы данных», а не замена бизнес-логики.

Использовать draft-07 или 2020-12?

Для новых проектов используйте 2020-12; для существующих систем проверьте, что поддерживает валидатор.

Версия Статус Ключевые изменения
draft-04 Устаревшая Многие старые библиотеки всё ещё по умолчанию используют её
draft-07 Фактический стандарт Самая широкая поддержка, богатейшая экосистема
2019-09 Переходная Введены $defs, unevaluatedProperties
2020-12 Сейчас рекомендуется items разделён на prefixItems + items, поддерживает $dynamicRef

Две ловушки миграции:

  1. Изменился синтаксис кортежей: в draft-07 items со значением-массивом выражал кортеж; 2020-12 перенёс это в prefixItems;
  2. $ref и соседние ключевые слова: в draft-07 соседние ключевые слова игнорируются при наличии $ref; 2020-12 позволяет им сосуществовать.

Если валидатор поддерживает только draft-07, написанный синтаксис 2020-12 будет молча игнорироваться — и самый опасный случай — «валидация проходит», когда на самом деле ничего не проверялось.

Как генерировать типы TypeScript из Schema?

Два маршрута: выводить типы из Schema или генерировать Schema из типов.

Из Schema в типы (бэкенд определяет контракт, фронтенд его использует):

import type { FromSchema } from 'json-schema-to-ts';

const userSchema = {
  type: 'object',
  properties: {
    id: { type: 'string' },
    age: { type: 'integer' },
  },
  required: ['id'],
  additionalProperties: false,
} as const;

type User = FromSchema<typeof userSchema>;
// { id: string; age?: number }

Ключевой момент — as const: без него type: 'string' выводится как string, а не как литерал 'string', и вывод ломается.

Из типов в Schema (у фронтенда уже есть типы и нужна валидация во время выполнения):

import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';

const userSchema = z.object({ id: z.string(), age: z.number().int().optional() });
const jsonSchema = zodToJsonSchema(userSchema);

Преимущество этого маршрута — «одно определение даёт и типы, и валидацию во время выполнения»; цена — введение DSL, а сгенерированная Schema может нести детали реализации (например, некрасивую структуру $ref).

Как найти конкретное поле, когда валидация не проходит?

Смотрите instancePath (2020-12) или dataPath (draft-07), возвращаемый валидатором; это JSON Pointer на место ошибки.

[
  {
    "instancePath": "/items/2/price",
    "schemaPath": "#/properties/items/items/properties/price/type",
    "keyword": "type",
    "message": "must be number"
  }
]

instancePath: "/items/2/price" прямо говорит: у поля price третьего элемента массива items неверный тип.

Два практических совета:

  1. Возвращайте все ошибки сразу, а не выбрасывайте первую — фронтенд, исправляющий всё за один проход, быстрее, чем попытки туда-обратно;
  2. Превращайте Pointer в понятный язык перед показом конечным пользователям; /items/2/price полезен разработчикам и бесполезен пользователям.

allErrors: true — это переключатель в большинстве валидаторов (Ajv требует явного включения); по умолчанию часто режим fail-fast.

Стоит ли включать additionalProperties: false?

Включайте для внутренних контрактов; будьте осторожны с публичными API.

После включения любое поле, не объявленное в properties, не проходит валидацию. Выгода — ловля опечаток (userNmae) и неожиданного сквозного прохождения; недостаток — добавление поля вышестоящей системой ломает нижестоящих потребителей, уничтожая обратную совместимость.

Компромисс:

Сценарий Рекомендация
Между внутренними сервисами Включить. Изменения полей проходят через управление версиями
Публичные внешние API Отключить. Позволить вышестоящей системе добавлять поля
Потребление сторонних данных Отключить. Вы не контролируете другую сторону
Валидация конфигурации Включить. Опечатка в конфигурации должна сразу падать

Как проверить уникальность элементов массива?

Две разные семантики — не путайте их:

{ "uniqueItems": true }

Это дедуплицирует весь элемент, требуя, чтобы массив не содержал двух глубоко равных элементов.

{
  "type": "array",
  "items": { "type": "object", "properties": { "id": { "type": "string" } } },
  "uniqueItems": true
}

Фрагмент выше гарантирует лишь то, что объекты целиком не дублируются; {"id":"a","name":"x"} и {"id":"a","name":"y"} не считаются дубликатами.

Дедуплицировать по конкретному полю Schema выразить не может; это нужно валидировать в бизнес-слое:

const ids = items.map((item) => item.id);
if (new Set(ids).size !== ids.length) {
  throw new Error('id 重复');
}

Действительно ли format валидирует email и даты?

Не обязательно. В спецификации format по умолчанию является «аннотацией», а не «утверждением», поэтому валидатор может собирать его, но не проверять.

  • Ajv требует установки и настройки отдельного пакета ajv-formats, иначе format: "email" фактически неактивен;
  • Некоторые валидаторы трактуют format как утверждение, включённое по умолчанию, поэтому поведение несогласовано;
  • Даже когда включён, регулярное выражение email очень свободное — a@b проходит.

Вывод: не считайте format строгой валидацией. Делайте строгую проверку email, URL и дат в бизнес-коде; хранить format в Schema как документацию полезно, но не ждите, что он остановит грязные данные.

3. Быстрый справочник

Вопрос Вывод
Найти узлы по условию JSONPath
Точная адресация, применение патчей JSON Pointer
Изменить элементы массива JSON Patch, Merge Patch этого не может
Слить конфигурацию Merge Patch
Версия Schema для новых проектов 2020-12, учитывайте prefixItems
Генерировать типы TS из Schema json-schema-to-ts + as const
Найти ошибки валидации Читайте instancePath, включайте allErrors
Дедуплицировать по полю Schema не может; используйте Set в бизнес-слое
format: email Большинство валидаторов не проверяют его по умолчанию, не полагайтесь на него