Проверка JSON: форматирование, синтаксис и JSON Schema с Ajv
Автор: Казачкин Даниил Михайлович · Обновлено
Красиво отформатированный JSON может содержать неправильные данные, а успешный JSON.parse не подтверждает соответствие интерфейсу TypeScript. Построим локальную проверку импорта учебного занятия: отдельно разберём синтаксис, проверим структуру по JSON Schema и только после этого выведем удобное представление.
Цель и предварительные знания
После урока вы сможете выбрать нужный инструмент для трёх разных запросов: JSON formatter, JSON syntax validator и JSON Schema validator. Нужны знания объектов, строк, исключений и границы между unknown и описанным типом. Пример работает на Node.js 24 с Ajv 8. Проверяющая программа написана на обычном JavaScript, чтобы показать: runtime-проверка не зависит от наличия TypeScript-аннотаций.
Три вопроса к одному документу
Форматирование отвечает за удобство чтения: отступы, переносы, представление вложенных объектов. Синтаксическая проверка отвечает, является ли текст допустимым JSON: правильно ли записаны строки, запятые и скобки. Проверка схемы отвечает, допустимо ли полученное значение по нашему контракту: присутствуют ли нужные поля, правильны ли их типы и ограничения.
Например, {"minutes":"45"} синтаксически корректен, но minutes является строкой. Если занятие должно хранить число минут, форматтер не исправит несоответствие. А строка с лишней запятой перед закрывающей скобкой не пройдёт даже JSON.parse. Схема не должна пытаться описывать значение, пока текст ещё не разобран.
Полностью локальный валидатор
Создайте отдельную папку с простым package.json. Установите зависимость следующей командой; сохранённый lockfile позволит повторять пример на той же версии Ajv.
{
"private": true,
"type": "module"
}pnpm add ajv@8Сохраните весь следующий фрагмент в validate.mjs. Выбран диалект JSON Schema 2020-12, поэтому используется соответствующий класс Ajv. Обычный основной экспорт Ajv рассчитан по умолчанию на другой диалект; одной строки $schema недостаточно, чтобы любой экземпляр автоматически стал подходящим валидатором.
import Ajv2020 from 'ajv/dist/2020.js';
const schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
properties: {
title: { type: 'string', minLength: 1 },
minutes: { type: 'integer', minimum: 1, maximum: 240 },
},
required: ['title', 'minutes'],
additionalProperties: false,
};
const ajv = new Ajv2020({ allErrors: true });
const validate = ajv.compile(schema);
const documents = [
'{"title":"Функции","minutes":45}',
'{"title":"Функции","minutes":"45"}',
'{"title":"Функции","minutes":45,}',
];
for (const document of documents) {
let data;
try {
data = JSON.parse(document);
} catch {
console.log('syntax:error');
continue;
}
if (!validate(data)) {
const problems = (validate.errors ?? [])
.map((issue) => `${issue.instancePath || '/'}:${issue.keyword}`)
.join(', ');
console.log(`schema:error ${problems}`);
continue;
}
console.log('schema:ok');
console.log(JSON.stringify(data, null, 2));
}node validate.mjsПервый документ даст schema:ok и объект с отступом в два пробела. Второй даст schema:error /minutes:type. Третий даст syntax:error. Таким образом, неверный тип поля и неправильная запятая оказываются в разных ветках. Проверка не обращается к внешнему сервису: все три строки и схема обрабатываются внутри запущенного процесса.
Что именно выражает схема
properties описывает правила для известных свойств, а required отдельно требует их наличия. Само перечисление minutes внутри properties не делает поле обязательным. additionalProperties: false отклоняет неизвестные поля: это наш явный выбор для компактного формата импорта, а не универсальная рекомендация для всех API.
Ограничения integer и диапазона запрещают строку, дробное значение, ноль и слишком большую длительность. Название должно содержать хотя бы один символ, но строка из пробелов пока удовлетворяет minLength. Схема описывает записанные правила, а не угадывает человеческое представление о хорошем названии. Этот пробел в контракте мы разберём в практике.
Экземпляр Ajv и скомпилированная функция создаются один раз перед циклом. Компилировать ту же схему для каждого документа незачем. Ошибки читаются сразу после конкретной проверки: следующий вызов может заменить значение errors. В приложении сохраняйте нужное представление ошибки, прежде чем переходить к следующему запросу или асинхронной операции.
От JSON к interface без ложной гарантии
Генератор JSON to TypeScript interface способен предложить форму по образцу: строковое title, числовое minutes. Но один документ не сообщает, какие поля иногда отсутствуют, где допустим null и какой диапазон чисел разрешён. Полученный интерфейс — гипотеза о статической модели, которую нужно сверить с контрактом и другими примерами.
Кроме того, interface не содержит исполняемой проверки, а обычный number не означает целое число от 1 до 240. Если JSON Schema является источником контракта, производные типы стоит получать и проверять согласованным инструментом, а не вручную поддерживать две независимые правды. Само утверждение as Lesson после JSON.parse не связывает данные со схемой.
В TypeScript-коде результат внешнего разбора разумно удерживать как unknown до проверки. После успешной валидации нужна корректно типизированная граница, согласованная со схемой. Не добавляйте произвольный generic к валидатору только ради удобного автодополнения: несоответствие объявленного интерфейса реальным правилам вернёт прежнюю проблему.
Типичные ошибки
Не называйте stringify проверкой содержимого: он сериализует значение, но не проверяет наш контракт. И не используйте пару parse/stringify как универсальный способ сохранить исходный текст без изменений: форматирование теряет исходные пробелы, а числовое представление JavaScript может быть недостаточным для больших точных идентификаторов. Такие идентификаторы часто следует хранить строками по договорённости формата.
Включение автоматического преобразования типов в валидаторе меняет семантику входа. Тогда строка с цифрами может стать числом, и пример перестанет демонстрировать строгое отклонение. Сначала решите, нужен ли контракту такой режим. Политика исправления данных должна быть явной, особенно когда разные клиенты передают разные формы одного поля.
Практика с разбором
Добавьте документы без title, с пустым названием, с названием из пробелов и с лишним полем room. До запуска запишите ожидаемые результаты. Затем потребуйте, чтобы title содержал хотя бы один непробельный символ. Не меняйте исходную строку автоматически: задача состоит в проверке, а не нормализации.
Разбор: отсутствие title нарушает required, пустая строка — minLength, room — additionalProperties. Пробелы пройдут исходную схему. Добавление строкового pattern \\S в JavaScript-объект правила title, то есть регулярного выражения «есть непробельный символ», отклонит и этот случай. Учтите два уровня записи: обратная косая черта экранируется в строковом литерале JavaScript. Проверяйте именно запущенный пример, а не визуальное сходство текста.
Частые вопросы
Может ли JSON быть числом или массивом на верхнем уровне?
Да. Синтаксический формат допускает значения, отличные от объекта. Наша схема отдельно требует type object, поэтому корректный JSON 42 будет отвергнут на этапе проверки контракта. Это наглядный пример различия между допустимым JSON и допустимым документом приложения.
Чем JSON Schema отличается от Zod?
JSON Schema — переносимое декларативное описание, для которого существуют валидаторы в разных языках. Zod описывает исполняемые схемы средствами JavaScript и удобно выводит TypeScript-типы. Выбор зависит от того, где живёт контракт и кто должен его исполнять; обе технологии требуют продуманного набора правил и тестов.