DarkRiDDeR15 мин

JSON Schema и бизнес-правила: где проходит граница валидации

BackendКонтракты данных

Проблема возникает, когда сервис принимает хорошо сформированный JSON, но отклоняет операцию позже: лимит оказался недоступен, курс валюты устарел, а ресурс уже изменился. Цена смешения слоёв — неясная ошибка 400/409, повторные попытки клиента и спор о том, где именно нарушен контракт.

Причина обычно в широком слове «валидировать». Им называют проверку JSON-типа, обязательных полей, доступа пользователя и текущего состояния базы одновременно. Такой обработчик трудно тестировать: непонятно, какой вход должен быть отклонён схемой, а какой — доменной проверкой. Разделим эти решения и соберём минимальный фильтр, который можно запустить без сервера.

Три слоя, которые нельзя склеивать

Первый слой — структура: объект, строка, число, массив, обязательность, формат и перечисление. JSON Schema хорошо подходит для такого вопроса. Второй слой — локальный инвариант: например, minAmount <= maxAmount или допустимый размер страницы. Его можно проверять кодом после разбора JSON, если правило зависит от нескольких полей. Третий слой — состояние системы: существует ли пользователь, не занят ли ресурс, не истёк ли токен. Этот слой требует доступа к данным и обычно возвращает другой класс ошибки.

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

Что проверять схемой, а что — кодом
СлойПримерРезультат ошибкиПодход
Тип и обязательностьlimit — integer, required400: malformed documentJSON Schema или генератор клиента
Диапазон1 ≤ limit ≤ 100400: invalid valueSchema minimum/maximum плюс тест
Связь полейfrom <= to400: inconsistent filterЧистая функция с двумя полями
Состояниересурс не изменён после чтения409: state conflictВерсия, условный запрос, транзакция
Правороль может менять статус403: forbiddenАвторизация до изменения состояния

Schema не делает неизвестное допустимым

У JSON Schema есть важное свойство: ограничения должны быть явными. Для API-фильтра можно разрешить limit, cursor и state, а остальные свойства закрыть через additionalProperties: false в нужном месте схемы. Но закрытость должна соответствовать расширяемости интерфейса. Если команда добавляет служебное поле без версионирования, строгая схема станет источником неожиданных отказов.

Есть и другая ловушка — использовать format как доказательство полной корректности. Формат даты или URI задаёт синтаксическую подсказку, но не подтверждает, что дата разрешена для операции или что URI принадлежит доверенному домену. Слово «valid» в отчёте должно иметь уточнение: valid по схеме, valid для инварианта или valid в текущем состоянии.

Матрица валидации: структура JSON, связь полей, состояние ресурса и право на действие проходят отдельные проверки с разными классами ошибок.
Схема помогает не выдавать успешный разбор JSON за разрешение операции. Каждый слой имеет собственный вход, сообщение и границу ответственности.

Runnable-пример: форма и инвариант по отдельности

В следующем фрагменте функция принимает фильтр поиска. Она проверяет форму и диапазон, добавляет безопасные значения по умолчанию и возвращает нормализованный объект. Это не библиотека JSON Schema, а маленький учебный аналог, на котором видно место бизнес-правила. Состояние базы и право доступа намеренно не притворяются частью результата.

import { validateFilterInput } from './upgrade-2027-09.mjs';

const good = validateFilterInput({ state: 'active', limit: 25 });
const bad = validateFilterInput({ state: 'active', limit: 250 });
const unknown = validateFilterInput({ state: 'active', region: 'eu' });

console.log(good.ok, good.value.limit, good.value.cursor);
console.log(bad.ok, bad.reason);
console.log(unknown.ok, unknown.value.state);
// true 25 null
// false limit-out-of-range
// true active

Порядок разложения проверки

  1. Опишите JSON-документ отдельно от команды, которая его использует. Назовите поля, типы, обязательность и допустимые значения.
  2. Выберите закрытую или расширяемую модель неизвестных полей. Решение должно быть одинаковым для сервера и клиентов, иначе один слой будет отвергать данные другого.
  3. Вынесите связи нескольких полей в чистые функции. Каждая функция должна иметь отрицательный пример и возвращать имя нарушенного правила.
  4. Присвойте класс ошибки: malformed input, invalid value, conflict или forbidden. Не превращайте конфликт состояния в повторную отправку 400.
  5. Проверьте, какие правила требуют чтения базы или другого сервиса. Для них зафиксируйте порядок проверки и условия гонки.
  6. Сверьте документацию и код на одном fixture-наборе. Расхождение между схемой и runtime-валидатором должно ломать сборку тестов.

Почему 409 важнее ещё одного boolean

Когда форма запроса корректна, но состояние изменилось, клиенту нужна возможность выбрать действие: перечитать ресурс, показать конфликт или прекратить операцию. Boolean вроде valid: false стирает причину. HTTP-семантика и локальный API-контракт должны различать ошибку документа и невозможность применить правильный документ к текущему состоянию.

Это различие помогает и с повторными попытками. Ошибка схемы не станет правильной от второго запроса, а конфликт иногда исчезает после нового чтения. Если оба случая имеют один статус, клиент либо повторяет бесполезную отправку, либо молча теряет возможность безопасного разрешения. Хорошая валидация уменьшает число retry-циклов именно тем, что сообщает границу отказа.

Ограничения и следующий шаг

Учебная функция не реализует полный draft 2020–12, не строит JSON Pointer к ошибке и не читает доменное состояние. Она показывает архитектурное разделение, а не заменяет валидатор библиотеки. В реальном API необходимо проверить выбранную библиотеку на oneOf, ссылки, форматы и поведение при неизвестных ключах.

Следующий шаг — взять один endpoint с конфликтом состояния и выписать три независимых теста: неправильная форма, нарушенный инвариант и устаревшая версия ресурса. После этого сравните их статусы и сообщения с документацией. Если один тест требует данных, которых нет в запросе, не расширяйте схему вслепую: это сигнал, что правило относится к сервисному слою.

Проверяемые источники

  • OpenAPI Specification 3.1.1 — OpenAPI Initiative, 24 октября 2024 года, версия 3.1.1. Применение: Фиксирует структуру HTTP-интерфейса, операции, ответы и семантику описания, чтобы контракт был машинно читаемым. Граница: Не доказывает, что сервер действительно отдаёт описанное тело: runtime-проверка и тесты остаются отдельной обязанностью.
  • JSON Schema Core 2020-12 — JSON Schema, draft 2020-12, спецификация Core. Применение: Задаёт язык типов, обязательных полей, ограничений и ветвления для JSON-документов. Граница: Схема не знает бизнес-состояние, права доступа, задержку или согласованность нескольких запросов.
  • RFC 9110 — HTTP Semantics — IETF, июнь 2022 года, RFC 9110, Standards Track. Применение: Разделяет метод, статус, представление ресурса и условия обмена, на которые опирается совместимость. Граница: Не описывает локальную реализацию сервиса, формат внутренней базы или конкретный клиент.