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

Главное: используйте 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 |
Две ловушки миграции:
- Изменился синтаксис кортежей: в draft-07
itemsсо значением-массивом выражал кортеж; 2020-12 перенёс это вprefixItems; $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 неверный тип.
Два практических совета:
- Возвращайте все ошибки сразу, а не выбрасывайте первую — фронтенд, исправляющий всё за один проход, быстрее, чем попытки туда-обратно;
- Превращайте 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 |
Большинство валидаторов не проверяют его по умолчанию, не полагайтесь на него |