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 и слияет несколько ошибок на одном поле в одно самое действенное сообщение.

/** Одна ошибка валидации, готовая к отображению */
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) : 'Значение неверно';
}

Три правила здесь важны:

  1. Не конкатенируйте предложения. «Неверный формат email» и его китайский эквивалент не разделяют порядок слов, и сборка строк в итоге даст бессмыслицу в одном из них.
  2. Передайте множественные числа и счётные слова библиотеке i18n. Никогда не хардкодьте «символов» в шаблоне; 1 символ и 3 символа различаются.
  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 в серверных логах: это поддерживает отладку, не раскрывая внутренние перечисления, регулярные выражения или раскладку файлов.