Files
progcode/editorial/agent-rewrites/011.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
20 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 11,
"slug": "editorial-2027-09-mechanism-mentor-series",
"title": "JSON Schema не разрешает операцию: как разделить форму, инвариант и состояние",
"excerpt": "Валидный JSON может описывать недопустимое действие. Разбираем границу между JSON Schema, проверкой связи полей и конфликтом текущего состояния ресурса.",
"contentHtml": "<p>Сервис принимает запрос на изменение заказа. JSON проходит проверку схемы: поля есть, типы верны, сумма положительная. Через несколько миллисекунд сервис отвечает отказом, потому что заказ уже оплатили или лимит клиента изменился. В логах оба случая часто выглядят одинаково: <code>validation failed</code>. Клиент повторяет запрос, оператор видит лишнюю операцию, а разработчик ищет ошибку то в схеме, то в базе. Цена ошибки — потерянное время и риск повторить действие, которое нельзя повторять.</p>\n<p>Причина в том, что слово «валидация» объединяет разные вопросы. JSON Schema отвечает, имеет ли документ допустимую форму. Чистая доменная проверка отвечает, согласованы ли его поля. Сервис проверяет, можно ли применить документ к текущему состоянию и имеет ли пользователь право на действие. Тезис статьи простой: границу нужно проводить по источнику контекста. Пока проверке не нужны база, часы, права или другой запрос, её можно держать на границе документа. Как только появляется внешний контекст, это уже правило операции.</p>\n<h2>Три вопроса вместо одного «valid»</h2>\n<p>Первый вопрос относится к форме. Запрос должен быть объектом, <code>limit</code> — целым числом от 1 до 100, а <code>state</code> — одним из заранее объявленных значений. Проверка не читает базу и не вызывает сеть. Для одинакового входа она всегда возвращает одинаковый ответ.</p>\n<p>Второй вопрос относится к связи полей. Например, диапазон дат требует, чтобы <code>from</code> не был позже <code>to</code>. Оба значения могут иметь правильный тип и формат, но вместе быть бессмысленными. Это правило можно выразить в схеме, если выбранный диалект и валидатор поддерживают нужную конструкцию. На практике его часто проще вынести в чистую функцию с отдельным тестом. Важно не место записи, а ясная граница ответственности.</p>\n<p>Третий вопрос относится к состоянию. Документ может быть безупречным, но ресурс уже изменился. Версия <code>revision: 4</code> устарела, купон исчерпан, а роль пользователя не позволяет перевести заказ в новый статус. Такой отказ нельзя исправить изменением JSON-типа. Клиенту нужно перечитать ресурс, показать конфликт или прекратить операцию.</p>\n<table><caption>Слой проверки и его граница</caption><thead><tr><th scope=\"col\">Слой</th><th scope=\"col\">Что проверяем</th><th scope=\"col\">Пример отказа</th><th scope=\"col\">Где выполнять</th></tr></thead><tbody><tr><td>Форма</td><td>Тип, обязательность, диапазон, enum</td><td><code>limit</code> не является integer</td><td>JSON Schema или валидатор на входе</td></tr><tr><td>Инвариант</td><td>Связь нескольких полей</td><td><code>from</code> позже <code>to</code></td><td>Чистая доменная функция</td></tr><tr><td>Состояние</td><td>Актуальность и доступность ресурса</td><td>Ревизия уже изменилась</td><td>Сервис, репозиторий, транзакция</td></tr><tr><td>Право</td><td>Разрешение на операцию</td><td>Роль не может отменить заказ</td><td>Авторизация до изменения состояния</td></tr></tbody></table>\n<h2>Что именно описывает JSON Schema</h2>\n<p>Схема полезна там, где нужно зафиксировать форму документа и дать одинаковую проверку нескольким потребителям. Она задаёт типы, обязательные ключи, диапазоны, шаблоны, перечисления и структуру вложенных объектов. Она также делает контракт читаемым для инструментов. OpenAPI 3.1 использует модель Schema Object, совместимую с JSON Schema 2020-12 с оговорёнными изменениями, поэтому форму HTTP-запроса удобно держать рядом с описанием endpoint.</p>\n<p>Схема не знает, что запись существует именно сейчас. Она не проверяет доступ к строке базы, не сравнивает версию с версией в хранилище и не гарантирует, что внешний сервис ответил успешно. Даже формат даты не равен бизнес-смыслу даты: строка может быть корректной датой, но лежать за пределами периода, в котором разрешена операция.</p>\n<p>Неизвестные поля требуют отдельного решения. Закрытая схема с запретом лишних ключей ловит опечатку, но может помешать расширению контракта. Открытая схема облегчает добавление полей, но пропускает ошибочный ключ, который клиент потом молча проигнорирует. Выберите модель для конкретного endpoint и закрепите её тестом. Не меняйте это поведение случайно при обновлении валидатора.</p>\n<figure><img src=\"/assets/editorial/2027/mentor-series-2027-practice-review-matrix.svg\" alt=\"Матрица проверки JSON: форма документа, инвариант, состояние ресурса и право на действие\" loading=\"lazy\" /><figcaption>Один запрос проходит несколько независимых границ. У каждой границы свой источник данных, класс ошибки и следующий шаг клиента.</figcaption></figure>\n<h2>Учебный пример: сначала форма, потом доменное правило</h2>\n<p>Ниже — самостоятельный учебный фрагмент без HTTP-сервера и базы данных. Он показывает порядок вычислений, а не готовый production-код. Первая функция проверяет форму фильтра. Вторая проверяет связь полей. Обе функции чистые: их результат зависит только от переданного объекта.</p>\n<pre><code>const filterSchema = {\n type: 'object',\n additionalProperties: false,\n properties: {\n limit: { type: 'integer', minimum: 1, maximum: 100 },\n from: { type: 'string', format: 'date' },\n to: { type: 'string', format: 'date' }\n }\n};\n\nfunction validateRange(input) {\n if (input.from &amp;&amp; input.to &amp;&amp; input.from &gt; input.to) {\n return { ok: false, reason: 'from-after-to' };\n }\n return { ok: true };\n}\n\nconst shapeIsValid = validateWithSchema(filterSchema, {\n limit: 25, from: '2027-09-10', to: '2027-09-12'\n});\nconst rangeIsValid = validateRange({\n from: '2027-09-12', to: '2027-09-10'\n});</code></pre>\n<p>В примере <code>validateWithSchema</code> обозначает вызов выбранной библиотекой JSON Schema. Это намеренное сокращение: конкретные API библиотек различаются, а задача фрагмента — показать две границы. Первый вызов отвечает за форму. Второй не пытается читать ресурс и не решает, есть ли право на поиск. В учебных данных нет утверждения о производительности или поведении в production.</p>\n<p>Для изменения ресурса добавляется третий шаг. Клиент отправляет ожидаемую ревизию. Сервис читает текущую ревизию внутри транзакции и применяет изменение только при совпадении. Если в хранилище уже другая версия, сервис возвращает конфликт. Простая проверка до транзакции не защищает от гонки: другой запрос может изменить запись после чтения.</p>\n<pre><code>async function updateOrder(command, actor) {\n const shape = validateCommandShape(command);\n if (!shape.ok) return httpError(400, shape.reason);\n\n const domain = validateCommandInvariant(command);\n if (!domain.ok) return httpError(422, domain.reason);\n\n if (!canEditOrder(actor, command.orderId)) {\n return httpError(403, 'forbidden');\n }\n\n return db.transaction(async (tx) =&gt; {\n const order = await tx.orders.getForUpdate(command.orderId);\n if (!order || order.revision !== command.expectedRevision) {\n return httpError(409, 'state-conflict');\n }\n return tx.orders.update(command.orderId, command.patch);\n });\n}</code></pre>\n<p>Этот код тоже ограничен учебной моделью. В реальной системе нужно проверить семантику блокировки, изоляцию транзакции, повторяемость команды и формат ошибки. Нельзя переносить фрагмент в production без проверки конкретного драйвера и правил домена.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Диагностика смешанной валидации</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Валидатор принимает ключ, а клиент его не использует</td><td>Схема открыта или контракт не обновили</td><td>Сверить unknown-key policy и чтение поля</td><td>Закрыть объект либо явно документировать расширение</td></tr><tr><td>Правильный JSON получает 400</td><td>Ошибка состояния скрыта под ошибкой формы</td><td>Разделить логи shape, invariant и state</td><td>Вернуть отдельный класс ошибки и исправить retry</td></tr><tr><td>Повторный запрос иногда меняет уже изменённый ресурс</td><td>Нет идемпотентности или проверки ревизии</td><td>Повторить команду при конкурентном обновлении</td><td>Добавить idempotency key или условие версии</td></tr><tr><td>Тест схемы проходит, endpoint падает</td><td>Схема не покрывает runtime-ветку</td><td>Вызвать реальный handler с теми же данными</td><td>Добавить интеграционный тест на границе</td></tr><tr><td>Клиент бесконечно повторяет запрос</td><td>Конфликт обозначен как временная ошибка</td><td>Проверить статус и тело ответа</td><td>Различить retryable отказ и конфликт ресурса</td></tr></tbody></table>\n<h2>Порядок внедрения</h2>\n<ol><li>Назовите endpoint, метод, формат запроса и ожидаемый ответ. Не начинайте с общей функции <code>validate</code>: сначала определите документ.</li><li>Выпишите поля, типы, обязательность, enum, диапазоны и политику неизвестных ключей. Сохраните положительный и отрицательный пример.</li><li>Отделите правила, которые используют только вход, от правил, которым нужны два поля, часы, права или данные ресурса.</li><li>Назначьте класс отказа. Неверная форма, нарушенный инвариант, запрет и конфликт состояния не должны превращаться в один boolean.</li><li>Проверьте отрицательный путь: устаревшая ревизия, повтор команды, отсутствие ресурса, запрещённая роль и сетевой отказ должны приводить к ожидаемому действию клиента.</li><li>Запустите проверку на реальном handler и на выбранной библиотеке схем. Unit-тест чистой функции не заменяет интеграционный тест.</li><li>Опишите безопасное расширение. Если добавляется поле или значение enum, проверьте старого потребителя и решите, нужен ли период совместимости.</li><li>Зафиксируйте наблюдаемый критерий готовности: каждый класс отказа имеет тест, статус, причину и понятный следующий шаг.</li></ol>\n<h2>Ограничения механизма</h2>\n<p>Разделение слоёв не устраняет сложность домена. Схема может стать слишком строгой. Чистая функция может повторить правило, которое уже проверяет база. Транзакция может быть недоступна для внешнего сервиса. Авторизация может зависеть от времени и нескольких систем. В таких случаях нужно явно описать границу и риск, а не расширять JSON Schema до роли универсального движка правил.</p>\n<p>Статус HTTP тоже не заменяет доменный контракт. <code>409 Conflict</code> подходит для конфликта с текущим состоянием ресурса, но тело ответа должно объяснить, что проверил сервис и что может сделать клиент. <code>422</code> может обозначать семантически неприемлемый документ, если это решение согласовано в API. Важна не магическая цифра, а стабильное различие между исправлением входа и разрешением конфликта.</p>\n<p>Нельзя заявлять, что схема доказала корректность операции. Проверяемый результат уже скромнее и полезнее: неправильная форма отсекается на границе, локальное правило имеет отдельный отрицательный тест, а конкурентное изменение ресурса не проходит без ясного конфликта. Это снижает конкретный риск, но не доказывает отсутствие всех дефектов.</p>\n<h2>Критерий готовности</h2>\n<p>Решение для выбранного endpoint готово, когда команда может показать четыре независимых теста: схема отклоняет неправильный тип, доменная функция отклоняет неверную связь полей, авторизация отклоняет запрещённого актёра, а транзакция отклоняет устаревшую ревизию. Для каждого теста указаны статус, причина и действие клиента. Положительный сценарий тоже проходит через тот же порядок. Если один из этих вопросов пока отвечает только фразой «валидатор всё проверит», граница ещё не проведена.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://json-schema.org/draft/2020-12\" target=\"_blank\" rel=\"noopener noreferrer\">JSON Schema Draft 2020-12</a> — официальная страница спецификации и документов Core/Validation. Используйте её для описания формы JSON; состояние ресурса и права остаются за пределами схемы.</li><li><a href=\"https://spec.openapis.org/oas/v3.1.1.html\" target=\"_blank\" rel=\"noopener noreferrer\">OpenAPI Specification 3.1.1</a> — официальное описание HTTP API и Schema Object. Сверяйте по нему endpoint, ответы и семантику документации.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110: HTTP Semantics</a> — официальный стандарт HTTP, включая смысл статуса 409 Conflict. Применяйте его как семантическую опору, а не как замену доменному контракту.</li></ul>"
}