JSON Toolkit

Коли помилки JSON Schema не мають сенсу: локалізація помилок Ajv і керування повідомленнями

Головне: Валідатор видає машинні помилки; користувачі хочуть знати, яке поле введення неправильне. Охоплює масив errors Ajv, instancePath проти schemaPath, групування й формулювання.

Просунутий~10 хв читанняОпубліковано 2026-10-31Оновлено 2026-10-31
Діаграма локалізації помилок перевірки Ajv і керування повідомленнями

Коротко: спершу читайте instancePath, щоб локалізувати збійні дані, потім schemaPath, щоб локалізувати правило, що спрацювало; для required і additionalProperties витягніть справжню назву ключа з params, перш ніж обирати повідомлення.

Робота валідатора — сказати, яке правило зазнало збою. Перетворити це на «це поле введення неправильне» — ваша робота.

1. Що насправді дає масив errors

Ajv не кидає виняток на збої перевірки; він залишає результат у validate.errors. Типова конфігурація зберігає лише першу помилку, а allErrors — це те, що дає повний список. Обʼєкт однієї помилки має пʼять важливих полів, і розуміння їх — передумова для всього подальшого:

Поле Значення Приклад
instancePath JSON Pointer до збійного значення /email
schemaPath Розташування збійного ключового слова у схемі #/properties/email/format
keyword Назва порушеного обмеження format
params Машинозчитувані параметри того обмеження { "format": "email" }
message Англійська проза, що змінюється між версіями must match format "email"

Справжня помилка виглядає так:

[
  {
    "instancePath": "/email",
    "schemaPath": "#/properties/email/format",
    "keyword": "format",
    "params": { "format": "email" },
    "message": "must match format \"email\""
  }
]

Зауважте, що message — це проза, написана для людей, а не стабільний інтерфейс. Ajv може переформулювати її між версіями, і вона не може обслуговувати локалізований продукт. Ніколи не розбирайте її регулярним виразом і ніколи не повертайте її дослівно клієнту.

2. instancePath і schemaPath відповідають на різні питання

instancePath каже, який зріз даних зазнав збою; schemaPath каже, яке правило спрацювало. Це дві системи координат, і вони не симетричні:

  • instancePath — це JSON Pointer RFC 6901. Порожній рядок означає корінь; /items/0/price означає поле price першого елемента масиву.
  • schemaPath починається з # і вказує на ключове слово всередині документа схеми. Через $ref він указує всередину схеми, на яку посилаються, тож вам потрібен $id, щоб знати, з якого файлу він походить.
  • Ключ, що містить / або ~, екранується до ~1 і ~0 відповідно. Скасуйте це перед показом, інакше UI покаже загадкові символи.

Наприклад, instancePath: "/email" разом із schemaPath: "#/properties/email/format" означає, що поле email на кореневому обʼєкті не пройшло перевірку формату.

Найпоширеніша пастка — required і additionalProperties: їхній instancePath вказує на батьківський обʼєкт, а не на проблемний ключ.

Ключове слово На що вказує instancePath Де живе справжній ключ
required Батьківський обʼєкт (порожній рядок у корені) params.missingProperty
additionalProperties Батьківський обʼєкт params.additionalProperty
type Саме збійне значення —
enum Саме збійне значення params.allowedValues

Відображаючи помилки на поля форми, спершу обробіть ці два випадки. Інакше «бракує email» стане «помилка кореневого обʼєкта», і користувач не зрозуміє, де вводити.

3. allErrors і verbose: повнота проти ціни

Типове allErrors: false змушує Ajv зупинятися на першому збої. Це добре для інструмента командного рядка й катастрофа для форми: користувач виправляє одне поле, надсилає, бачить наступну помилку, і повторює тричі, перш ніж форма проходить.

Налаштування Поведінка Перевага Ціна
allErrors: false Лише перша помилка Найшвидше, найменша памʼять Повторні надсилання
allErrors: true Збирати кожну помилку Усі проблеми за один прохід Кількість помилок вибухає на складних схемах
verbose: true Долучити schema і data до кожної помилки Легша діагностика на сервері Значно зростає розмір; ніколи не показуйте це назовні
messages: false Відкинути поле message Менші навантаження Втрачаєте англійську підказку для налагодження

Рекомендований поділ: увімкніть allErrors: true для серверної перевірки й тримайте повний масив у внутрішніх логах, тоді як публічна відповідь несе лише поля з білого списку. Увімкніть verbose лише під час розслідування; він вбудовує зіставлений фрагмент схеми й дані користувача в кожну помилку, що коштує памʼяті й може злити чутливі значення в логи.

Обсяг помилок — інша поширена проблема. Збійна гілка anyOf видає кілька дочірніх помилок плюс одну підсумкову, а oneOf із багатьма гілками ще гірше. Покажіть масив як є — і користувачі побачать пʼять майже дубльованих повідомлень для одного поля. Групування має відбуватися перед відображенням.

4. Групуйте помилки за полями

Групування робить три речі: перетворює шлях назад на назву поля, виправляє зміщення required і additionalProperties та обʼєднує кілька помилок на тому самому полі в одне найбільш дієве повідомлення.

/** One validation error ready for display */
interface FieldError {
  field: string;
  keyword: string;
  params: Record<string, unknown>;
}

/**
 * Group Ajv errors by field.
 * @param errors Error array produced by Ajv
 * @returns Map keyed by field path, each value listing its errors
 */
function groupByField(errors: Array<Record<string, any>>): Record<string, FieldError[]> {
  const grouped: Record<string, FieldError[]> = {};

  for (const errorItem of errors) {
    const keyword: string = errorItem.keyword;
    let field: string = errorItem.instancePath;

    if (keyword === 'required') {
      const parentPath: string = errorItem.instancePath;
      field = `${parentPath}/${errorItem.params.missingProperty}`;
    } else if (keyword === 'additionalProperties') {
      field = `${errorItem.instancePath}/${errorItem.params.additionalProperty}`;
    }

    const normalizedField = field === '' ? '(root)' : field;
    (grouped[normalizedField] ??= []).push({
      field: normalizedField,
      keyword,
      params: errorItem.params ?? {},
    });
  }

  return grouped;
}

Зливайте за пріоритетом: спершу required (відсутність), потім type, потім enum/const, потім format/pattern, потім довжина й діапазон. Показуйте лише повідомлення з найвищим пріоритетом на поле, а решту тримайте в логах. Причина проста: коли тип неправильний, повідомлення про довжину — шум, і виправлення типу — єдиний розумний наступний крок.

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

5. Власні повідомлення: шлях плагіна і шлях шаблону

Ajv не постачає локалізованих формулювань, тож є два практичні варіанти.

Варіант один: плагін ajv-errors. Він додає ключове слово errorMessage, щоб повідомлення жили поряд з обмеженнями:

{
  "properties": {
    "email": {
      "type": "string",
      "format": "email",
      "errorMessage": {
        "format": "Please enter a valid email address",
        "type": "Email must be a string"
      }
    }
  }
}

Перевага — формулювання подорожує з полем, а схема слугує й документацією. Недолік — текст заморожений в одній мові, тож локалізація означає підтримку кількох схем. Плагін також замінює всі повідомлення, лише коли встановлено allErrors: true; забудьте цей прапорець — і отримаєте суміш власних і типових англійських.

Варіант два: таблиця «ключове слово → шаблон». Валідатор надає лише keyword і params; застосунок продукує формулювання:

Ключове слово Шаблон Доступні параметри
required Missing required field: {field} missingProperty
type Expected type: {type} type
minLength Enter at least {limit} characters limit
maximum Must not exceed {limit} limit
format Invalid format: {format} format
enum Must be one of the allowed options allowedValues
pattern Value does not match the expected format pattern (не повторюйте його)

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

6. Локалізація: повідомлення не належать валідатору

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

/** Validation message templates keyed by Ajv keyword */
const VALIDATION_TEMPLATES: Record<string, (params: Record<string, any>) => string> = {
  required: (params) => `Missing required field: ${params.missingProperty}`,
  type: (params) => `Expected type: ${params.type}`,
  minLength: (params) => `Enter at least ${params.limit} characters`,
  maximum: (params) => `Must not exceed ${params.limit}`,
  format: () => 'Invalid format',
};

/**
 * Build one user-facing validation message.
 * @param keyword Name of the failed constraint
 * @param params Constraint parameters
 * @returns Localized message text
 */
function buildMessage(keyword: string, params: Record<string, any>): string {
  const template = VALIDATION_TEMPLATES[keyword];
  return template ? template(params) : 'The value is invalid';
}

Три правила важливі тут:

  1. Не склеюйте речення. «Email format is invalid» і його український відповідник не поділяють порядок слів, і складання рядків зрештою дасть безглуздя в одному з них.
  2. Доручіть множину й одиниці виміру бібліотеці i18n. Ніколи не вбудовуйте «characters» у шаблон; 1 character і 3 characters різняться.
  3. Перекладайте й мітки полів. params.missingProperty — це email, тоді як UI має казати «Адреса email». Тримайте це відображення в конфігурації форми, а не в мовному пакеті.

7. Не повертайте внутрішню структуру клієнтам

Повернення schemaPath, params і message разом зручне під час розробки й є витоком інформації у виробництві. allowedValues розкриває внутрішні переліки, pattern розкриває регулярні вирази перевірки, а schemaPath розкриває макет файлів і найменування полів — у деяких випадках даючи атакувальникам мапу ваших слабких місць.

Публічна відповідь тримає рівно три речі: шлях поля (або назву поля форми), стабільний код помилки й орієнтований на користувача текст. Використовуйте короткі стабільні коди замість сирого keyword, щоб зберегти простір змінити валідатор пізніше.

{
  "type": "about:blank",
  "title": "Request validation failed",
  "status": 422,
  "errors": [
    { "field": "email", "code": "invalid_format", "message": "Please enter a valid email address" },
    { "field": "items[0].price", "code": "invalid_pattern", "message": "Amount must have exactly two decimals" }
  ]
}

Серверні логи мають тримати протилежне: повний масив помилок, id запиту й версію схеми, щоб розслідування могло відповісти «яке правило це заблокувало». Уникайте вивалювати цілі тіла запитів у логи; тримайте збійні шляхи й ключові слова.

8. Подання на фронтенді: від підсвічування поля до маркерів редактора

Помилки зрештою потрапляють у UI, і подання залежить від контексту:

Контекст Подання Ключова дія
Форма Червоний текст під полем, фокус на першій помилці Відобразіть шляхи на назви форми; збережіть індекси масивів
Масовий імпорт таблиці Номер рядка, назва стовпця, підсумок, експортованість Перетворіть /items/3/price на рядок 4, стовпець price
Редактор JSON Вбудовані маркери й панель проблем Використайте парсер, щоб відобразити JSON Pointer на рядок і стовпець
Інструмент налагодження API Структурований список помилок, що зберігає schemaPath Орієнтований на розробника, тож більше деталей прийнятно

Три нотатки щодо реалізації:

  • Показуйте лише першу помилку на поле, згортаючи решту в «ще 2 проблеми», щоб форма не потонула в червоному.
  • Зберігайте індекси масивів для націлювання, але відкидайте їх у мітках. «Рядок 4, price» читається краще за /items/3/price.
  • Сценарії редактора потребують відображення позицій. Простий пошук рядка ненадійний, коли назва поля повторюється; парсер має надавати зміщення для кожного вузла, які ви потім перетворюєте на рядок і стовпець.

Якщо JSON надходить від користувача, що набирає, а не з форми, спершу підтвердьте, що документ синтаксично валідний. Синтаксичні помилки трапляються під час розбору й ніколи не досягають перевірки схеми. Розбір і форматування JSON повідомляє точний рядок і стовпець, що набагато швидше за гонитву за помилками схеми. Про те, як виникають надто строгі схеми, коли їх виводять зі зразків, дивіться Чому автоматично згенерована JSON Schema постійно відхиляє реальні дані; про конфігурацію валідатора й систему ключових слів дивіться JSON Schema: від основ до виробництва.

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

Чому я отримую лише одну помилку?

allErrors типово дорівнює false, тож Ajv повертає результат на першому збої. Увімкніть його явно для перевірки форм; інакше користувачам доведеться надсилати повторно, щоб виявити кожну проблему.

Чому шлях у помилці required порожній?

instancePath для required вказує на батьківський обʼєкт, що в корені є порожнім рядком; назва відсутнього ключа сидить у params.missingProperty. Склейте обидва, щоб отримати повний шлях. Показ лише instancePath дає некорисну «помилку кореневого обʼєкта».

Чи можна показувати message безпосередньо користувачам?

Не бажано. Це англійська проза, формулювання якої змінюється між версіями, і вона не має можливості локалізації. Генеруйте текст із keyword і params і залишайте message для логів.

Чому моє власне повідомлення про помилку ігнорується?

Спершу перевірте, чи ввімкнено allErrors: true; ajv-errors може замінити всі повідомлення лише з цим прапорцем. Потім підтвердьте, що errorMessage сидить на тому самому рівні, що й збійне ключове слово — на один рівень далі, і його не розпізнають.

Які поля безпечно повертати у виробництві?

Шлях поля, стабільний код помилки й орієнтований на користувача текст. Тримайте schemaPath, params і message в серверних логах: це підтримує налагодження без розкриття внутрішніх переліків, регулярних виразів чи макета файлів.