Часті питання про запити й перевірку: 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» →
testJSON 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 |
Більшість валідаторів типово його не перевіряють, тож не покладайтеся |