Когда ошибки 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 и слияет несколько ошибок на одном поле в одно самое действенное сообщение.
/** Одна ошибка валидации, готовая к отображению */
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 |
Отсутствует обязательное поле: {field} | missingProperty |
type |
Ожидаемый тип: {type} | type |
minLength |
Введите не менее {limit} символов | limit |
maximum |
Не должно превышать {limit} | limit |
format |
Неверный формат: {format} | format |
enum |
Должно быть одним из допустимых вариантов | allowedValues |
pattern |
Значение не соответствует ожидаемому формату | pattern (не отражайте его) |
Вариант второй подходит продуктам, которым нужны несколько языков и согласованный голос. Он разделяет валидацию и представление, держит перевод вне схемы, и позволяет фронтенду переиспользовать те же шаблоны.
6. Локализация: сообщения не принадлежат валидатору
Перемещение формулировки в языковые пакеты — отправная точка управления сообщениями. Языковой пакет хранит шаблоны и ключи, никогда имена полей; метки полей приходят из конфигурации формы или контракта API, и оба объединяются во время отрисовки.
/** Шаблоны сообщений валидации с ключом по ключевому слову Ajv */
const VALIDATION_TEMPLATES: Record<string, (params: Record<string, any>) => string> = {
required: (params) => `Отсутствует обязательное поле: ${params.missingProperty}`,
type: (params) => `Ожидаемый тип: ${params.type}`,
minLength: (params) => `Введите не менее ${params.limit} символов`,
maximum: (params) => `Не должно превышать ${params.limit}`,
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) : 'Значение неверно';
}
Три правила здесь важны:
- Не конкатенируйте предложения. «Неверный формат email» и его китайский эквивалент не разделяют порядок слов, и сборка строк в итоге даст бессмыслицу в одном из них.
- Передайте множественные числа и счётные слова библиотеке i18n. Никогда не хардкодьте «символов» в шаблоне;
1 символи3 символаразличаются. - Переводите и метки полей.
params.missingProperty— этоemail, тогда как UI должен говорить «Адрес электронной почты». Держите это отображение в конфигурации формы, а не в языковом пакете.
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 в серверных логах: это поддерживает отладку, не раскрывая внутренние перечисления, регулярные выражения или раскладку файлов.