JSON Toolkit

JSON Schema: від основ до виробництва

Головне: Ключових слів багато, але щодня використовують лише десяток. Правильно розберіться з type/properties/required/$ref, добре обробіть повідомлення про помилки — і покриєте вісімдесят відсотків реальних випадків.

Середній~14 хв читанняОпубліковано 2026-10-30Оновлено 2026-10-30
Обкладинка статті «JSON Schema: від основ до виробництва»

Коротко: спершу закріпіть $schema на 2020-12, потім виділіть спільні структури через $defs + $ref; використовуйте oneOf, лише коли має збігтися рівно одна гілка, а в усіх інших випадках надавайте перевагу дискримінаторам або anyOf.

Схема — не додаток до документації; це актив, який можна виконувати, тестувати й генерувати.

1. Спершу розберіться з версією

Версія Значення $schema Статус Примітки
draft-04 http://json-schema.org/draft-04/schema# Застаріла Застарілі проєкти й деякий інструментарій OpenAPI 2.0
draft-07 http://json-schema.org/draft-07/schema# Широко використовується Де-факто основа OpenAPI 3.0
2019-09 https://json-schema.org/draft/2019-09/schema Доступна Ввела $defs і словник анотацій
2020-12 https://json-schema.org/draft/2020-12/schema Нині рекомендована Ввела prefixItems, змінила семантику items

Для нових проєктів використовуйте 2020-12. Зауважте, що в 2020-12 items обмежує лише «рештку елементів» — оголошення в стилі кортежу перейшли до prefixItems. Це найпоширеніша пастка під час міграції з draft-07.

2. Мінімальна придатна схема

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/user.json",
  "type": "object",
  "required": ["id", "name"],
  "properties": {
    "id": { "type": "string", "minLength": 1 },
    "name": { "type": "string", "minLength": 1, "maxLength": 64 },
    "age": { "type": "integer", "minimum": 0, "maximum": 150 },
    "tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
  },
  "additionalProperties": false
}

Ця схема покриває чотири найпоширеніші потреби: обмеження типів, обовʼязкові поля, обмеження діапазону й обмеження елементів масиву.

3. Мапа ключових слів

3.1 Загальні

Ключове слово Призначення Застереження
type Тип; може бути масивом "type": ["string", "null"] означає «може бути null»
enum Перелічені значення Значення мають бути унікальними
const Обмеження одним значенням Добре для фіксованих значень, як-от номер версії
$ref Посилається на іншу схему Посилається на схему цілком і не зливається з сусідніми ключовими словами обʼєкта-посилання

3.2 Числа

multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum. Зауважте, що в draft-04 exclusiveMaximum був булевим (використовувався разом із maximum); лише з draft-07 він став самостійним числовим ключовим словом — через це багато «схем, копійованих зі старої документації», зазнають збою в строгому режимі.

3.3 Рядки

minLength, maxLength, pattern (регулярний вираз ECMA-262) і format (анотація).

minLength рахує кодові точки Unicode, а не одиниці UTF-16 і не байти. Перевірка через string.length у JavaScript дасть розбіжність.

3.4 Масиви

Ключове слово Призначення
prefixItems Обмеження в стилі кортежу 2020-12, зіставляє елементи один за одним за індексом
items Однорідне обмеження решти елементів (2020-12)
contains + minContains/maxContains Скільком збігам треба бути присутніми (щонайменше/щонайбільше)
uniqueItems Елементи мають усі різнитися (глибоке порівняння; уникайте на великих масивах)
minItems / maxItems Обмеження довжини

3.5 Обʼєкти

properties, required, patternProperties, additionalProperties, propertyNames, minProperties, maxProperties, dependentRequired, dependentSchemas.

Три часті хибні уявлення:

  1. required лише вимагає існування ключа: {"name": ""} валідний. Щоб відхилити порожній рядок, треба додати minLength: 1;
  2. additionalProperties: false збільшує проблеми сумісності: одне зайве поле від клієнта провалює все навантаження, тож еволюція API змушує кожного викликача оновлюватися синхронно;
  3. properties не обмежує неоголошені ключі — за замовчуванням вони дозволені.

3.6 Логічна композиція

{
  "oneOf": [
    { "type": "object", "required": ["cardNumber"], "properties": { "cardNumber": { "type": "string" } } },
    { "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string" } } }
  ]
}
Ключове слово Семантика Типовий збій
allOf Усі мають бути задоволені Кілька гілок накладають суперечливі обмеження на те саме поле
anyOf Щонайменше одна має бути задоволена Повідомлення про помилку важко простежити до конкретної гілки
oneOf Рівно одна має бути задоволена Дані збігаються з двома гілками й зазнають збою, хоча «виглядають очевидно правильними»
not Не повинна бути задоволена Рідко використовується, і найменш дружні повідомлення про помилки

oneOf — ключове слово, найсхильніше до «валідних даних, що провалюють перевірку». Якщо обмеження гілок перетинаються (наприклад, обидві дозволяють зайві поля), один запис збігається з кількома гілками, і oneOf просто зазнає збою. У такому разі перемкніться на anyOf або використайте дискримінаторне поле з const усередині гілок.

{
  "type": "object",
  "required": ["kind"],
  "properties": { "kind": { "enum": ["text", "image"] } },
  "if": { "properties": { "kind": { "const": "image" } } },
  "then": { "required": ["url", "width", "height"] },
  "else": { "required": ["content"] }
}

Стиль із дискримінатором читабельніший за oneOf і дає кращі помилки.

4. Поділ і повторне використання

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/order.json",
  "$defs": {
    "money": { "type": "string", "pattern": "^-?\\d+\\.\\d{2}$" },
    "address": {
      "type": "object",
      "required": ["city"],
      "properties": { "city": { "type": "string" }, "detail": { "type": "string" } }
    }
  },
  "type": "object",
  "required": ["total", "shipTo"],
  "properties": {
    "total": { "$ref": "#/$defs/money" },
    "shipTo": { "$ref": "#/$defs/address" }
  }
}
  • $defs — форма, введена в 2019-09; draft-07 використовує definitions;
  • Міжфайлові посилання використовують відносні $id, але не покладайтеся на завантаження віддалених схем під час виконання у виробництві — пакуйте їх як статичні файли;
  • Не можна додавати обмеження поряд із посиланням (поведінка $ref із сусідніми ключовими словами різниться за версіями). Коли потрібна композиція, використовуйте allOf.

5. Перевірка під час виконання через Ajv

import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import orderSchema from './order.json' with { type: 'json' };

const ajv = new Ajv2020({
  allErrors: true,
  strict: true,
  allowUnionTypes: true,
});
addFormats(ajv);

const validateOrder = ajv.compile(orderSchema);

export function assertOrder(input: unknown): void {
  if (validateOrder(input)) {
    return;
  }
  const detail = (validateOrder.errors ?? []).map((errorItem) => ({
    path: errorItem.instancePath || '/',
    keyword: errorItem.keyword,
    message: errorItem.message,
    params: errorItem.params,
  }));
  throw new Error(`Order data failed validation: ${JSON.stringify(detail)}`);
}

Ключові моменти:

  • Викликайте compile лише один раз, на верхньому рівні модуля; повторна компіляція на кожен запит перетворює 10-мілісекундну перевірку на сотні мілісекунд;
  • strict: true рано виявляє неправильно написані ключові слова (наприклад minlength замість minLength), тож його варто ввімкнути;
  • Лише ajv-formats робить format: "email" і format: "date-time" такими, що справді діють;
  • 2020-12 потребує точки входу ajv/dist/2020 — не використовуйте типовий експорт.

6. Як проєктувати повідомлення про помилки

Валідатор дає вам «яке ключове слово зазнало збою»; користувач хоче «яке поле введення неправильне». Варто додати шар відображення:

const FIELD_LABEL: Record<string, string> = {
  '/name': 'Name',
  '/age': 'Age',
  '/tags': 'Tags',
};

/**
 * Convert Ajv errors into display-ready messages.
 * @param errors The list of Ajv errors
 * @returns An array of display messages
 */
function toMessages(errors: Array<{ instancePath: string; keyword: string; params: unknown }>): string[] {
  return errors.map((errorItem) => {
    const label = FIELD_LABEL[errorItem.instancePath] ?? errorItem.instancePath;
    if (errorItem.keyword === 'required') {
      const missing = (errorItem.params as { missingProperty: string }).missingProperty;
      return `${label} is missing the required field ${missing}`;
    }
    if (errorItem.keyword === 'minLength') {
      return `${label} is too short`;
    }
    return `${label} failed validation (${errorItem.keyword})`;
  });
}

На боці API тримайте машинозчитувану структуру, щоб фронтенд міг вирішити, як її подати:

{
  "type": "https://bbdrug.com/problems/validation",
  "title": "Request data validation failed",
  "status": 422,
  "errors": [
    { "path": "/items/0/price", "keyword": "pattern", "message": "Must be a monetary string with two decimal places" }
  ]
}

7. Продуктивність і пастки

  • Ціна перевірки зростає зі складністю схеми й розміром даних; кілька гілок oneOf спричиняють комбінаторний вибух, який стає вельми помітним на тисячах записів;
  • uniqueItems виконує глибоке порівняння O(n²) над масивом; для великих масивів дедуплікуйте в бізнес-шарі;
  • Уникайте шаблонів катастрофічного відкочування в pattern (як-от (a+)+$); це класична точка входу для ReDoS;
  • Коли additionalProperties: false поєднується з allOf, поля, оголошені в підсхемі, вважаються «зайвими» батьком, тож треба злити їх в одну схему або використати unevaluatedProperties.

8. Чек-лист

  • [ ] Закріпіть 2020-12 і оголосіть $schema на початку файлу;
  • [ ] Виділяйте спільні структури через $defs + $ref, а не копіюйте-вставляйте;
  • [ ] Надавайте перевагу дискримінаторам над oneOf;
  • [ ] Повторно використовуйте результат compile валідатора й увімкніть строгий режим;
  • [ ] Чітко документуйте, чи format забезпечується;
  • [ ] Відповіді про помилки надають шлях, ключове слово й орієнтоване на користувача повідомлення;
  • [ ] Покладіть схему в CI: запускайте випадки JSON Schema Test Suite як регресійні тести.

Часті питання

Як обрати чернетку JSON Schema?

Для нових проєктів використовуйте 2020-12 і оголосіть $schema на початку файлу. Якщо схема потрапляє в OpenAPI 3.1, вона теж мусить бути 2020-12, бо 3.1 вбудовує цю версію безпосередньо; залишайте draft-07 лише для проєктів, усе ще привʼязаних до старішого інструментарію. Дві версії поводяться по-різному з items і сусідніми ключовими словами $ref, тож не змішуйте їх в одному наборі схем.

Чому additionalProperties: false збільшує проблеми сумісності?

Воно трактує кожен неоголошений ключ як помилку, тож одне зайве поле від клієнта провалює все навантаження. Кожен викликач тоді має оновлюватися синхронно при кожній еволюції API, інакше старі клієнти відхиляються. Схеми в стилі конфігурації мають явно закривати зайві поля, а публічні API потребують зваження ціни сумісності.

Використовувати oneOf чи anyOf?

Використовуйте oneOf, лише коли дані мусять збігтися рівно з однією гілкою. Якщо обмеження гілок можуть виконуватися одночасно, один запис збігається з кількома гілками, і oneOf просто зазнає збою; перемкніться на anyOf або додайте дискримінатор const усередині кожної гілки.