Перейти к содержимому
Поиск
Разработка

Ajv и JSON Schema: проверка JSON — мастерская и 9 практических задач

Девять задач на настоящем Ajv: клиент, заказ, адреса, варианты, формы и NDJSON. Русские ошибки, нормализация копии, мини-мастерская и исходники с Node API для Windows/Linux.

Александр Некрасов
Ajv и JSON Schema — проверка данных для API и форм

Ajv и JSON Schema на практике: девять живых задач, ошибки по полям, нормализация копии и импорт. Мини-мастерская, Node API, CLI и исходники для Windows/Linux.

Ajv: проверить смысл структуры JSON до выполнения операции

JSON успешно разобрался, но приложение всё равно сломалось: возраст пришёл строкой, в заказе отсутствует количество, адрес содержит неизвестное поле, а дата не существует. JSON.parse подтверждает синтаксис документа. Он не знает, какие данные ожидает ваш сервис и что именно можно принять в конкретной операции.

Ajv проверяет данные по JSON Schema. Схема описывает типы, обязательные поля, размеры массивов, диапазоны, варианты объектов и другие правила структуры. Вы компилируете её в функцию, затем используете эту функцию для повторных проверок. Результат содержит понятный признак успеха и структурированные ошибки, которые можно показать рядом с полями или сохранить в отчёте.

В статье девять практических задач: клиент, лишние поля, заказ, адреса, варианты доставки, форматы, форма, нормализация и построчный импорт. На странице работает настоящий Ajv. В архиве находятся тот же проверяющий модуль, Node HTTP API, CLI, примеры запросов и инструкции для Windows, Linux и VPS.

Где применять: API, импорт, конфигурация и формы

Первое применение — граница HTTP API. До обращения к базе и выполнения бизнес-действия полезно понять, что пришёл объект нужной формы: известный тип заявки, предусмотренные поля, ограниченный массив и допустимые числа. Тогда ошибка контракта превращается в осмысленный ответ, а не в случайное исключение глубоко внутри обработчика.

Второе применение — импорт выгрузки. Плохая строка не обязательно должна останавливать весь разбор: можно сохранить её номер и причины, продолжить проверку остальных, а затем отдельно принять корректные записи. В мастерской это показано на NDJSON. Для настоящего импорта после структуры потребуются дополнительные проверки связей, дубликатов и правил сохранения.

Третье применение — конфигурация и интерфейс. Одну доверенную схему можно использовать в Node и в браузере, чтобы давать раннюю подсказку человеку и повторять проверку на сервере. Клиентская проверка повышает удобство, но её можно обойти. Поэтому решение о допустимости рабочей операции всегда остаётся у серверного приложения.

JSON Schema, TypeScript, Zod и прикладные проверки

Тип TypeScript помогает разработчику при проверке кода, но сам по себе не проверяет внешний JSON во время выполнения. Ajv работает именно с фактическим значением. JSON Schema удобно хранить как отдельный контракт, передавать между инструментами и применять в системах, которым нужен общий формат описания структуры.

В мастерской Zod показан другой удобный подход к проверке данных в JavaScript и TypeScript. Выбор зависит от окружения: нужен ли самостоятельный JSON Schema, какие инструменты его уже используют и где живут правила. Для PHP-части полезен отдельный материал о Symfony Validator.

Ни один валидатор структуры автоматически не подтверждает права пользователя, уникальность email, остаток товара или доступность внешнего сервиса. Объект может идеально соответствовать схеме и при этом быть недопустимым для конкретного аккаунта. Разделяйте структурную проверку, нормализацию и бизнес-операцию, чтобы сообщения и ответственность каждого этапа оставались понятными.

function checkDocument(text, validate) {
  const parsed = JSON.parse(text); // только синтаксис JSON
  const valid = validate(parsed); // структура по схеме
  if (!valid) {
    return {ok: false, errors: structuredClone(validate.errors ?? [])};
  }
  return {ok: true, data: parsed};
}
// Права, доступность и бизнес-правила проверяются следующим этапом.

Версия схемы: почему здесь используется Ajv2020

В проекте закреплены Ajv 8.20.0 и ajv-formats 3.0.1. Все схемы объявляют draft 2020-12, поэтому импортируется конструктор Ajv2020 из ajv/dist/2020.js. Это осознанная связка, а не декоративное поле $schema в JSON. Версия языка определяет допустимые ключевые слова и их смысл.

Обычный экспорт Ajv по умолчанию связан с draft-07. Нельзя без проверки смешивать схемы 2020-12 и более ранних версий в одном экземпляре. Например, для кортежей в 2020-12 используется prefixItems, а старый массив items не переносится механически. Для массива одинаковых позиций нашего заказа items остаётся схемой одного элемента.

Перед использованием внешнего контракта уточняйте его draft и возможности валидатора. Соответствие инструментов важнее наличия одинакового слова JSON в названии. Различия версий подробно перечислены в официальном справочнике JSON Schema в Ajv; исходники мастерской явно фиксируют выбранную версию.

import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
const ajv = new Ajv2020({strict: true, allErrors: true});
addFormats(ajv);
const schema = {
  $schema: 'https://json-schema.org/draft/2020-12/schema',
  type: 'object',
  properties: {name: {type: 'string', minLength: 1}},
  required: ['name'],
  additionalProperties: false
};
const validate = ajv.compile(schema);

Мини-мастерская: девять задач и свои ограниченные данные

Выберите задачу и сначала загрузите корректный пример. Затем нажмите «Данные с ошибками», прочитайте причины и исправьте JSON самостоятельно. Результат показывает путь проблемного поля и русское пояснение. Технические поля Ajv сохраняются в скачиваемом JSON-отчёте, поэтому удобный текст не уничтожает сведения для диагностики.

Для формы ошибки расположены рядом с полями. Для нормализации есть отдельный переключатель: исходный JSON остаётся слева, а изменённая копия видна в результате. В NDJSON проверяются отдельные строки; один неразбираемый объект не мешает показать результат остальных. После ручного изменения входа старый отчёт отключается до повторной проверки.

Механизм указан прямо на странице: настоящий Ajv исполняется в браузере, данные остаются во вкладке, схемы заранее заданы автором. Произвольные внешние схемы и код здесь не компилируются. HTTP API и CLI из архива работают отдельно на вашем компьютере или Linux-стенде. На телефоне мастерскую можно открыть самостоятельной страницей.

Windows: установка, просмотр и первый Node-пример

Нужен поддерживаемый Node.js 24 LTS не ниже 24.15. Этот минимум учитывает не только приложение, но и инструменты проверки проекта. Распакуйте архив, откройте PowerShell в ajv-project и выполните команды ниже. Версии зависимостей восстанавливаются из package-lock.json, готовая мастерская открывается на http://127.0.0.1:8402/.

npm start поднимает Node-сервер, который отдаёт public и отдельный API. При этом сама веб-мастерская продолжает проверять данные локально в браузере: запуск Node не превращает её действия в серверные запросы автоматически. Настоящий HTTP вы проверите отдельной командой ниже. Сервер привязан к loopback и останавливается Ctrl+C в своём терминале.

Для первого запуска CLI сервер вообще не требуется. Корректный customer.json даёт успешный отчёт, а import.ndjson содержит намеренно ошибочную строку и возвращает код 1. Это предусмотренный учебный результат. Сравнивайте JSON-отчёт и код завершения, чтобы не перепутать отказ данных с ошибкой установки.

node --version
npm.cmd ci --ignore-scripts
npm.cmd test
npm.cmd run build
npm.cmd start
# Отдельно, без обязательного HTTP-сервера:
node src/cli.mjs customer examples/customer.json
$LASTEXITCODE
node src/cli.mjs import examples/import.ndjson
$LASTEXITCODE

Linux: тот же контракт в консоли и на сервере

На Linux используется тот же проект и lock-файл. После установки Node.js 24 LTS выполните npm ci, test и build обычным пользователем. CLI читает файл, проверяет данные и выводит JSON. Это удобно для подготовленного импорта, проверки конфигурации перед запуском и небольшого шага автоматизации.

Код завершения нужно сохранять сразу после проверяющего процесса. Следующая команда может заменить его собственным результатом. В нашем CLI 0 означает успешную проверку всех данных, 1 — несоответствие схеме или отклонённые строки NDJSON, включая неразбираемую строку, 2 — ошибку чтения, синтаксиса одиночного JSON, аргументов или общих ограничений. Такой договор позволяет отличать неправильные данные от не состоявшейся проверки.

Для первого просмотра на VPS удобно запустить loopback-стенд и открыть его через SSH-туннель. Для постоянного Node endpoint ниже разобраны systemd и reverse proxy. Если нужен только браузерный инструмент, public можно обслуживать статически: серверная проверка не является обязательной частью просмотра мастерской.

npm ci --ignore-scripts
npm test
npm run build
node src/cli.mjs customer examples/customer.json
echo $?
node src/cli.mjs import examples/import.ndjson
echo $?
# Для отдельного просмотра на VPS:
# npm start
# На рабочем компьютере:
ssh -L 8402:127.0.0.1:8402 user@your-server

Компилируйте схему один раз, проверяйте много раз

Ajv компилирует описание схемы в проверяющую функцию. Эту подготовку не нужно повторять для каждого клиента или каждой строки импорта. В проекте экземпляр Ajv и набор validators создаются при загрузке модуля, а обработчик запроса только выбирает известную функцию и вызывает её.

Такое устройство упрощает и эксплуатацию: неподдерживаемое ключевое слово или ошибка доверенной схемы обнаруживаются при старте, а не случайно у одного пользователя. Схемы развёртываются вместе с кодом и проходят тесты. Для нормализации создан отдельный экземпляр с явно отличающимися опциями, чтобы обычная проверка не начала неожиданно менять данные.

Не создавайте новый объект схемы и новый Ajv внутри горячего цикла без необходимости. Если схем действительно много, продумайте управление их идентификаторами и кешем. Рекомендации по повторному использованию функций и предварительной подготовке есть в Managing schemas. Мастерская использует простой предварительно скомпилированный набор из девяти задач.

// Инициализация модуля:
const validators = new Map(knownSchemas.map(({key, schema}) =>
  [key, ajv.compile(schema)]
));
// Обработка данных:
function checkKnown(key, data) {
  const validate = validators.get(key);
  if (!validate) throw new Error('Неизвестная схема');
  const valid = validate(data);
  return {valid, errors: structuredClone(validate.errors ?? [])};
}

Пример 1. Клиент: наличие поля, тип и пустое значение

required проверяет наличие свойства. Это не то же самое, что непустая строка, ненулевое число или осмысленный текст. В первом опыте name обязан присутствовать и иметь длину хотя бы один, email — строковый формат почты, age — целое число в диапазоне от 18 до 120.

Удалите name, затем верните его как пустую строку и как null. Причины будут разными: отсутствующее свойство, недостаточная длина и неверный тип. Возраст строкой “29” тоже не принимается в обычном режиме. Это полезно на границе API, где похожие человеку значения могут иметь разный технический смысл.

Схема не обрезает пробелы сама по себе. Имя из одних пробелов может удовлетворять minLength, хотя не подходит вашему продукту. Если нужен trim или другое преобразование, определите отдельный этап и проверяйте уже выбранный результат. Не приписывайте структурному правилу больше смысла, чем в нём действительно записано.

{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email","maxLength":254},"age":{"type":"integer","minimum":18,"maximum":120}},"required":["name","email","age"],"additionalProperties":false}

Пример 2. Лишние поля: опечатка не должна тихо пройти

Второй опыт принимает name и active. Добавьте admin или случайно напишите actve. additionalProperties:false запрещает непредусмотренный ключ, а required отдельно заметит отсутствие нужного поля. Такой договор полезен для конфигурации и API, где лишнее поле чаще означает ошибку клиента, чем полезное расширение.

По умолчанию запрет не означает удаление поля. Обычная проверка возвращает ошибку и сохраняет исходные данные. Удаление относится к отдельной опции removeAdditional, которую мы включаем только в опыте нормализации копии. Смешивать эти два поведения опасно: разработчик может ожидать строгого отказа, а получить молча изменённый объект.

Политика дополнительных полей зависит от контракта. Иногда совместимость требует принимать расширения, иногда нужен закрытый объект. Для составных схем учитывайте расположение properties и правила конкретного draft; механическое добавление additionalProperties:false к любому allOf не является универсальным рецептом. В мастерской структуры специально просты и проверены.

{"type":"object","properties":{"name":{"type":"string","minLength":1},"active":{"type":"boolean"}},"required":["name","active"],"additionalProperties":false}

Пример 3. Заказ: ограничения массива и каждой позиции

В заказе недостаточно проверить, что items является массивом. Нужно определить, может ли он быть пустым, сколько элементов допустимо и как выглядит каждая позиция. Наш пример разрешает от одной до двадцати позиций, ограничивает количество товара и проверяет код SKU и числовую цену.

Загрузите ошибочный пример: сразу появятся причины для SKU, quantity и price. Путь /items/0/quantity относится к полю первого объекта массива. Он не зависит от того, на какой строке красиво отформатирован JSON в редакторе. Такое различие важно для интерфейса импорта и для автоматического связывания ошибки с данными.

Числовой диапазон цены не делает расчёт денег точным и не подтверждает доступность товара. Схема описывает форму входа. Прикладной код отдельно проверяет единицы, валюту, склад, скидки и способ вычисления сумм. Разделение этих этапов помогает вернуть человеку именно ту причину, которая действительно помешала операции.

const orderSchema = {
  type: 'object',
  properties: {
    items: {type: 'array', minItems: 1, maxItems: 20, items: {
      type: 'object',
      properties: {
        sku: {type: 'string', pattern: '^[A-Z][A-Z0-9-]{1,15}$'},
        quantity: {type: 'integer', minimum: 1, maximum: 100},
        price: {type: 'number', minimum: 0, maximum: 1000000}
      },
      required: ['sku', 'quantity', 'price'], additionalProperties: false
    }}
  },
  required: ['items'], additionalProperties: false
};

Пример 4. $defs и $ref: одно правило для двух адресов

Адрес доставки и адрес оплаты имеют одинаковую структуру. Если копировать правила вручную, они постепенно расходятся: в одном месте появляется ограничение, а в другом остаётся старая версия. $defs позволяет описать адрес один раз, а $ref — использовать это определение в нескольких полях.

В опыте refs оба адреса требуют город и шестизначный индекс. Исправьте shipping, оставив ошибку в billing: отчёт покажет конкретный путь каждого нарушения. Общая схема не делает адреса одним объектом данных, она только повторно использует правила проверки.

Все ссылки мастерской локальны: #/$defs/address разрешается внутри доверенной схемы. Здесь нет автоматической загрузки произвольного URL. $id служит идентификатором схемы и не означает обязательный сетевой запрос. Если вы добавляете внешние определения, заранее регистрируйте доверенный набор и отдельно продумывайте правила его обновления.

const addressSchema = {
  type: 'object',
  $defs: {address: {
    type: 'object',
    properties: {city: {type: 'string', minLength: 1},
      postcode: {type: 'string', pattern: '^[0-9]{6}$'}},
    required: ['city', 'postcode'], additionalProperties: false
  }},
  properties: {shipping: {$ref: '#/$defs/address'}, billing: {$ref: '#/$defs/address'}},
  required: ['shipping', 'billing'], additionalProperties: false
};

Пример 5. oneOf: самовывоз или доставка, ровно один вариант

Заявка самовывоза требует pointId, а доставка — address. oneOf принимает данные, соответствующие ровно одной ветке. Поле kind с разными const делает варианты однозначными: pickup и delivery. Пользователь получает один понятный тип операции, а сервер может выбрать соответствующую обработку.

В ошибочном примере указано kind:delivery, но передан pointId. Исправьте поля и посмотрите, как меняется результат. При allErrors Ajv может показать ошибки нескольких веток и итоговое нарушение oneOf. Это не обязательно несколько независимых проблем пользователя: часть записей объясняет, почему неподходящие варианты не совпали.

Для удобного продукта полезно переводить такие технические результаты в сообщение выбранного сценария, сохраняя полный отчёт для диагностики. Не меняйте oneOf на anyOf только ради исчезновения ошибки, если бизнес-контракт требует единственный вариант. Смысл альтернатив должен быть определён до реализации формы.

const requestSchema = {type: 'object', oneOf: [
  {properties: {kind: {const: 'pickup'}, pointId: {type: 'integer', minimum: 1}},
   required: ['kind', 'pointId'], additionalProperties: false},
  {properties: {kind: {const: 'delivery'}, address: {type: 'string', minLength: 1}},
   required: ['kind', 'address'], additionalProperties: false}
]};

Пример 6. Форматы: email, дата и время с часовым поясом

Пакет ajv-formats добавляет проверки распространённых форматов. В шестом опыте можно сравнить корректный email, календарную дату и date-time. Дата 2026-02-30 не существует, а дата-время без часового пояса не соответствует выбранному формату. Исправляйте каждое значение и наблюдайте отдельные причины.

Проверка email относится к синтаксису. Она не отправляет письмо, не подтверждает владение адресом и не гарантирует существование ящика. Аналогично корректная дата не доказывает, что событие находится в будущем или что интервал свободен. Эти условия требуют контекста приложения.

Указывайте ожидаемый формат рядом с полем и определяйте, как хранить время. Если пользователь вводит локальные часы, приложение должно осмысленно связать их с часовым поясом. Не превращайте строку в UTC случайным добавлением Z. Поддерживаемые форматы и способы подключения перечислены в руководстве Ajv по форматам.

addFormats(ajv);
const formatsSchema = {
  type: 'object',
  properties: {
    email: {type: 'string', format: 'email', maxLength: 254},
    date: {type: 'string', format: 'date'},
    startsAt: {type: 'string', format: 'date-time'}
  },
  required: ['email', 'date', 'startsAt'], additionalProperties: false
};

Пример 7. Форма: сообщения рядом с полями

В режиме form JSON собирается из обычных полей имени, email и возраста. Ошибка показывается непосредственно у соответствующего ввода, поле получает aria-invalid, а aria-describedby связывает его с пояснением. Общий список остаётся полезным для просмотра всех причин, но человеку не нужно искать свою ошибку в длинном техническом объекте.

HTML input отдаёт строковое значение. В этой форме возраст явно преобразуется через Number, а пустой ввод становится null. Это действие интерфейса, не скрытая работа coerceTypes. Различие важно: иначе читатель может решить, что обычный Ajv автоматически превращает любую числовую строку в число.

После исправления данные проверяются заново, старое состояние ошибки очищается. В рабочем продукте выбирайте подходящий момент проверки: немедленно, после выхода из поля или при отправке. Не мешайте человеку вводить ещё не завершённое значение чрезмерно навязчивыми сообщениями, но оставляйте понятный путь исправления перед сохранением.

const data = {
  name: nameInput.value,
  email: emailInput.value,
  age: ageInput.value === '' ? null : Number(ageInput.value)
};
const result = validateData('form', data);
// Русский текст — слой интерфейса; raw errors сохраняются в отчёте.

JSON Pointer: путь к данным, а не координата текста

Ajv возвращает instancePath. Путь /items/0/quantity означает свойство quantity первого элемента items. Для required путь часто указывает родительский объект, а имя отсутствующего свойства находится в params.missingProperty. Для additionalProperties имя лишнего поля также передаётся отдельно. Мастерская добавляет удобное path для этих случаев.

В JSON Pointer символы имени имеют собственное экранирование: тильда превращается в ~0, косая черта — в ~1. Свойство с буквальным именем a/b не является вложенным объектом a с полем b. Если забыть это различие, подсветка попадёт не туда или сообщение будет вводить человека в заблуждение.

Пустой instancePath обозначает корень документа. UI подписывает его словами, а raw-значение остаётся в отчёте. Привязка к полям формы должна учитывать её конкретную модель, особенно массивы, динамические строки и имена с необычными символами. Не пытайтесь превратить любой Pointer в CSS-селектор без разбора и безопасного обращения к DOM.

const escapePointer = key => String(key).replaceAll('~', '~0').replaceAll('/', '~1');
function fieldPath(error) {
  if (error.keyword === 'required') {
    return error.instancePath + '/' + escapePointer(error.params.missingProperty);
  }
  return error.instancePath;
}
// Имя a/b~c представляется сегментом a~1b~0c.

Копируйте ошибки сразу: следующий запуск обновляет validate.errors

Скомпилированная функция используется повторно. Её поле errors относится к последнему вызову и заменяется при следующей проверке. Если отложенный обработчик позже снова читает validate.errors, он может получить результат уже другого клиента. Ссылка на ранее полученный массив остаётся ссылкой на прежний массив; structuredClone дополнительно создаёт независимый снимок вложенных объектов ошибок.

В engine мастерской ошибки копируются сразу после вызова. Это особенно полезно для пакетного импорта, асинхронного сохранения отчёта и нескольких действий интерфейса. Сохраняются исходные keyword, params, schemaPath, instancePath и message; русское пояснение строится отдельно и не перезаписывает технический текст.

Наличие полного списка ошибок удобно в учебном ограниченном примере. Для публичного production API режим allErrors требует отдельной оценки стоимости, о чём поговорим ниже. Не превращайте учебный отчёт с большим количеством причин в универсальную настройку любого запроса.

const valid = validate(data);
const errors = structuredClone(validate.errors ?? []);
const report = {valid, errors};
// Дальнейший вызов validate(otherData) не меняет этот report.

Пример 8. Нормализация: изменяется только явная копия

Восьмой опыт сравнивает обычную проверку и отдельный режим нормализации. Исходная конфигурация содержит retries строкой, отсутствующий enabled и лишний extra. Без переключателя данные отклоняются и остаются прежними. После включения копия получает число, значение по умолчанию и удаление непредусмотренного поля.

Важный момент — нормализуется structuredClone, а исходный объект сохраняется. Поэтому можно сравнить до и после и не потерять вход для диагностики. Опции coerceTypes, useDefaults и removeAdditional включены только в отдельном экземпляре Ajv и только для известной схемы normalize.

Это не универсальное поведение стандарта JSON Schema и не обещание безопасно исправить любой ввод. Пользователь должен понимать, что сервис меняет данные. Для рабочих настроек часто полезнее показать предлагаемые изменения и сохранить результат только после успешной проверки и прикладного решения.

const normalizer = new Ajv2020({
  strict: true, allErrors: true,
  coerceTypes: true, useDefaults: true, removeAdditional: true
});
const validateNormalized = normalizer.compile(configSchema);
const output = structuredClone(input);
const valid = validateNormalized(output);
// input остаётся исходным; output принимается только при valid:true.

Тонкости изменения данных: defaults не равен автосохранению

Изменяющая проверка может частично преобразовать копию, а затем завершиться неуспешно. Поэтому факт changed:true ещё не разрешает использовать output как корректную конфигурацию. Сначала нужен valid:true, затем прикладные условия и только потом сохранение. Отдельный статус помогает не спутать преобразование с подтверждением результата.

Корневой примитив не изменяется через передачу аргумента так же, как свойства объекта. В мастерской нормализуется объект конфигурации, поэтому изменение retries и добавление enabled наблюдаются в копии. Не переносите это ожидание без проверки на вызов validate с отдельной строкой или числом.

При составных схемах преобразования могут зависеть от порядка оценки. Не используйте removeAdditional внутри сложных альтернатив наугад. Официальный раздел Modifying data описывает ограничения и непереносимость таких опций. Для сложного импорта разумно разделить явное преобразование и последующую неизменяющую проверку.

const result = validateData('normalize', input, {mutate: true});
if (!result.valid) {
  // Даже изменённая копия ещё не принята.
  showErrors(result.errors);
} else {
  reviewNormalizedConfiguration(result.output);
}

Пример 9. NDJSON: проверить строки и собрать отчёт

NDJSON хранит один JSON-документ на физическую строку. Такой формат удобен для последовательных выгрузок и небольших импортов. В опыте можно смешать корректный объект, неразбираемый JSON и объект с ошибками схемы. Мастерская продолжит разбор и сохранит исходный номер каждой непустой строки.

Пустые строки пропускаются, но не перенумеровывают последующие проблемы. В отчёте есть количество принятых и отклонённых объектов, а отдельный просмотр показывает данные только принятых строк. Это результат проверки, не транзакция сохранения в базе: приложение ещё должно решить, можно ли импортировать часть набора или требуется целостность всей операции.

Учебный импорт ограничен 64 КиБ и двумястами физическими строками. Для больших файлов нужен потоковый разбор, лимиты на строку, контроль памяти, отчёт и политика повторного запуска. Не удаляйте ограничения только потому, что текущие три строки быстро прошли проверку.

{"name":"Анна","email":"anna@example.test","age":29}
{"name":"","email":"bad","age":15}
{"name":"Иван","email":"ivan@example.test","age":32}

CLI: ограниченное чтение файла и предсказуемые коды

CLI из архива читает обычный файл с заранее проверенным размером и дополнительно ограничивает фактическое чтение. Это важно: проверка после readFile всего многогигабайтного файла уже не защищает память. Если размер изменится между проверкой и чтением, буфер всё равно не вырастет сверх выбранного предела.

Для одной JSON-записи выбирается ключ схемы, для import включается построчная обработка. Результат выводится в stdout, сообщения о невозможности проверки — в stderr. Это позволяет использовать инструмент в автоматизации без обязательного HTTP-сервера и отдельно хранить отчёт.

Пример ниже показывает логику CI для Linux. Сначала сохраняется код проверяющего процесса, затем отчёт можно опубликовать средствами вашей системы. Последний успешный шаг загрузки файла не должен случайно превратить отклонённый импорт в зелёный результат. В интерактивном PowerShell смотрите LASTEXITCODE сразу после Node.

set +e
node src/cli.mjs import examples/import.ndjson > import-report.json
validation_exit=$?
set -e
# Опубликуйте import-report.json средствами своей CI-системы.
exit "$validation_exit"

Настоящий Node HTTP API: конверт key и data

Endpoint POST /api/validate принимает известный ключ схемы и объект data. Для normalize можно явно передать mutate:true. Сервер не компилирует схему из запроса и не позволяет подставить произвольный внешний контракт. Он выбирает функцию из заранее подготовленного набора.

Ответ содержит valid, errors, input, output, changed и mode. Такая подробность полезна для учебного сравнения. В рабочем API возвращайте только необходимые поля и не отражайте автоматически секреты или персональные данные. Проверяющий endpoint ничего не записывает и не заменяет обработчик реального сохранения клиента.

В проекте есть готовые examples/request.json и normalize-request.json. Они содержат именно конверт API, в отличие от customer.json, предназначенного для CLI. Это небольшое различие часто становится причиной первой ошибки: корректный объект клиента ещё не является корректным HTTP-запросом к выбранному маршруту.

{"key":"customer","data":{"name":"Анна","email":"anna@example.test","age":29}}

HTTP-статусы и воспроизводимые запросы Windows/Linux

Сервер возвращает 200 для подходящих данных и 422, когда JSON разобран, но контракт нарушен. Статус 400 относится к неправильному JSON, конверту, неизвестной схеме или пределу сложности; 413 — к слишком большому телу; 415 — к неправильному Content-Type; 405 — к неподдерживаемому методу.

В Windows используйте curl.exe явно: это избегает путаницы с PowerShell alias. Тело удобно передавать из готового файла, сохраняя кавычки, Unicode и точную структуру. На Linux тот же запрос выполняется curl. Учитывайте, что без –fail сам curl не делает свой код ненулевым только из-за HTTP 422.

Не превращайте любую неудачу в одно сообщение «не работает API». Ошибка синтаксиса требует исправления JSON, нарушение контракта — исправления данных, а недоступный порт — проверки запуска сервера. Понятная диагностика экономит время читателю и помогает использовать пример за пределами страницы статьи.

# PowerShell, из каталога проекта при работающем npm start:
curl.exe -i -H "Content-Type: application/json" --data-binary "@examples/request.json" http://127.0.0.1:8402/api/validate
# Linux: та же структура запроса, команда curl вместо curl.exe

Пределы ввода и доверие к схемам

Ajv рассматривает схему как доверенную часть приложения. Компиляция произвольной схемы от посетителя создаёт другой класс рисков и затрат. В мастерской такой возможности нет: пользователь редактирует данные, а схемы фиксированы. Все ссылки разрешаются локально, никакой внешний loadSchema не запускается.

Вход ограничен размером, глубиной и количеством узлов. Импорт дополнительно ограничен числом строк. Отдельная учебная политика запрещает служебные ключи __proto__, prototype и constructor; это правило нашего импорта, а не универсальный запрет JSON Schema. Доверенные regex просты и известны заранее.

Официальные рекомендации безопасности Ajv отдельно предупреждают о стоимости регулярных выражений, форматов и продолжении проверки с allErrors. Здесь allErrors:true выбран для учебного отчёта на ограниченном наборе. Для публичной production-границы пересмотрите эту настройку и ограничения; один maxLength не делает любой regex безопасным.

const LIMITS = {bytes: 65536, depth: 24, nodes: 4000, lines: 200};
// HTTP: предел относится ко всему телу key/data.
// CLI: размер проверяется до чтения, само чтение тоже ограничено.
// Схемы принимаются только из доверенного набора приложения.

Браузерная компиляция и строгая CSP

Ajv создаёт проверяющие функции из схем. В браузере это влияет на совместимость с Content Security Policy, запрещающей такой способ генерации функций. Успешная локальная сборка не означает, что тот же runtime автоматически запустится на сайте с более строгой политикой.

Для фиксированных схем можно рассмотреть предварительную standalone-компиляцию на этапе сборки. Браузер тогда получает подготовленные функции вместо необходимости компилировать схему при открытии страницы. Этот подход также помогает сделать жизненный цикл подготовки более явным, хотя требует отдельной настройки сборки и проверки плагинов.

Не ослабляйте глобальную CSP рабочего сайта только ради переноса учебного примера. Сначала выберите подходящую архитектуру проверки. Мастерская честно использует настоящий Ajv runtime в браузере; отдельный вариант предварительной генерации описан в официальном руководстве Standalone validation code.

Linux/VPS: service user, loopback и reverse proxy

Для постоянного учебного API разместите проект в /srv/ajv, установите зависимости и соберите public. В архиве есть systemd unit с отдельным пользователем, loopback-портом 8402 и ограничением записи. Путь Node и каталога нужно проверить под свою систему; файл сервиса не устанавливает Node и не создаёт пользователя автоматически.

Для внешнего доступа используйте существующий защищённый HTTPS-вход. В deploy есть примеры Caddy и Nginx; Nginx дополнительно ограничивает тело, а Node повторно проверяет его размер. Эти фрагменты не являются готовой авторизацией. Базовые настройки подробнее разобраны в статьях о Caddy и Nginx.

Схемы обновляются вместе с кодом и тестами. Полезно развёртывать отдельный release с возможностью отката, проверять старт и пример запроса перед переключением. Не выдавайте запись всему проекту и не открывайте внутренний порт только ради просмотра. Для первого удалённого опыта достаточно SSH-туннеля, описанного выше.

# Пользователь создаётся один раз; пути подставьте свои.
sudo useradd --system --home-dir /srv/ajv --shell /usr/sbin/nologin ajv
sudo install -m 0644 deploy/ajv-api.service /etc/systemd/system/ajv-api.service
sudo systemctl daemon-reload
sudo systemctl start ajv-api
sudo systemctl status ajv-api --no-pager
curl -I http://127.0.0.1:8402/
journalctl -u ajv-api -n 50 --no-pager
# После отдельного учебного опыта:
sudo systemctl stop ajv-api

Изменение контракта: тесты нужны не только для хорошего объекта

У схемы есть потребители. Новое обязательное поле может сломать старый клиент, запрет дополнительных полей — отклонить прежнее расширение, а изменение типа — нарушить импорт. Рассматривайте схему как версионируемый договор, а не случайный набор условий внутри формы.

Сохраняйте корректные и ошибочные примеры для каждой существенной границы: отсутствующее поле, null, неверный тип, пустой массив, лишний ключ, предельный размер. Для oneOf проверяйте каждую допустимую ветку и неоднозначные варианты. Для нормализации отдельно подтверждайте, что исходник не изменён и что неуспешный output не принимается.

В проекте также исполняются все скачиваемые Node-рецепты, реальные HTTP-запросы и CLI. UI-проверки подтверждают сброс устаревшего отчёта и связь ошибок с полями. Это полезнее проверки только количества строк в файле: читатель должен получить исходники, которые воспроизводят описанное поведение.

npm test
npm run build
npm audit --audit-level=low
# Дополнительная ручная проверка выбранного контракта:
node src/cli.mjs customer examples/customer.json

Преобразование, проверка и сохранение: понятный порядок

Импорт нередко требует преобразовать внешний формат в внутреннюю модель. Для этого может использоваться отдельный код или JSONata. Важно понимать, какой объект вы проверяете: исходную выгрузку, промежуточное представление или готовую команду приложения.

Один полезный порядок — ограничить и разобрать вход, проверить внешний контракт, выполнить явное преобразование, проверить внутреннюю модель и только затем применять бизнес-правила и сохранение. Порядок может отличаться по задаче, но он должен быть осмысленным. Иначе автоматическое исправление легко скрывает дефект исходной системы.

Храните в отчёте достаточно контекста для исправления, но не собирайте лишние персональные данные. Номер строки, код правила и безопасное описание часто полезнее полного дампа клиента. Учебный отчёт подробный ради понимания механизма; при переносе в продукт состав диагностических данных нужно выбрать отдельно.

// Схема процесса, функции относятся к вашему приложению:
const raw = parseAndBound(inputText);
assertExternalContract(raw);
const command = transformToInternalModel(raw);
assertInternalContract(command);
await checkBusinessRules(command);
await saveCommand(command);

Что скачать и как начать применять в своём проекте

В мастерской можно скачать JSON Schema выбранного опыта, Node-файл validate.mjs и полный отчёт. Маленький файл содержит доверенную схему и авторский корректный объект; он показывает запуск Ajv, а не автоматически повторяет любой введённый посетителем JSON. Полный проект включает CLI, HTTP API, NDJSON и все девять сценариев.

Начните с одной границы приложения: конкретный API-запрос, файл настроек или импорт клиента. Опишите поля, допустимые варианты и ограничения, затем проверьте и хорошие, и плохие данные. Согласуйте язык ошибок с интерфейсом и сохраните технический отчёт для диагностики. После этого подключайте проверку к реальному сохранению.

Ajv полезен, когда контракт делает поведение системы понятнее. Схема помогает быстро обнаружить неправильную структуру, а мини-мастерская позволяет увидеть каждую причину на собственном примере. Сохраняйте различие между синтаксисом JSON, соответствием схеме, изменением копии и разрешением бизнес-операции — тогда инструмент будет приносить практическую пользу и в браузере, и на Linux-сервере.