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

Коротко: спершу читайте 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';
}
Три правила важливі тут:
- Не склеюйте речення. «Email format is invalid» і його український відповідник не поділяють порядок слів, і складання рядків зрештою дасть безглуздя в одному з них.
- Доручіть множину й одиниці виміру бібліотеці i18n. Ніколи не вбудовуйте «characters» у шаблон;
1 characterі3 charactersрізняться. - Перекладайте й мітки полів.
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 в серверних логах: це підтримує налагодження без розкриття внутрішніх переліків, регулярних виразів чи макета файлів.