8 lines
16 KiB
JSON
8 lines
16 KiB
JSON
{
|
||
"index": 12,
|
||
"slug": "editorial-2027-09-practice-mentor-series",
|
||
"title": "Совместимый API-ответ: как поймать breaking change до релиза",
|
||
"excerpt": "Разбираем ответ HTTP-метода на границе сервиса: какие изменения ломают старого клиента, как отделить форму JSON от состояния системы и чем доказать совместимость.",
|
||
"contentHtml": "<p>Сервис возвращает HTTP 200, но клиент падает при разборе ответа. В логах появляется ошибка чтения поля, неизвестное значение перечисления или попытка вызвать метод у числа вместо строки. Сервер считает запрос успешным, а потребитель — нет. Цена ошибки растёт быстро: приходится искать все версии клиента, откатывать серверный код и решать, не потеряны ли уже записи, созданные по новой схеме.</p>\\n<p>Причина обычно не в синтаксической ошибке JSON. Команда меняет форму ответа как внутреннюю модель и не замечает потребителей. Она удаляет поле, делает новое поле обязательным, меняет тип или добавляет значение в enum. Каждый такой diff имеет собственный риск. Тезис простой: API-ответ нужно проверять как контракт двух сторон — по форме, по поведению старого клиента и по условиям, которые схема не описывает.</p>\\n<h2>Что именно обещает ответ</h2>\\n<p>Контракт начинается с конкретной границы: метод, путь, статус, media type и тело. Для <code>GET /customers/{id}</code> можно зафиксировать объект с обязательными полями <code>id</code>, <code>revision</code> и <code>state</code>. У <code>id</code> строковый тип. У <code>revision</code> положительное целое число. У <code>state</code> закрытый набор значений <code>active</code> и <code>blocked</code>.</p>\\n<p>Эта форма отвечает на вопрос «можно ли разобрать JSON». Она не отвечает на вопросы «имеет ли пользователь право видеть клиента» и «не устарела ли ревизия записи». Эти проверки относятся к авторизации и состоянию. Если смешать их со схемой, ответ об ошибке станет неточным: клиент не поймёт, нужно ли исправить запрос, обновить данные или прекратить повторные попытки.</p>\\n<pre><code>GET /customers/{id}\\nAccept: application/json\\n\\n200 OK\\nContent-Type: application/json\\n\\n{\\n "id": "customer-17",\\n "revision": 4,\\n "state": "active"\\n}</code></pre>\\n<p>Успешный ответ не становится совместимым только потому, что его принимает парсер JSON. Клиент может ветвить логику по <code>state</code>, строить URL из <code>id</code> и сравнивать <code>revision</code> с локальной версией. Сохранить синтаксис недостаточно: нужно сохранить значения и смысл, на которые опирается старый код.</p>\\n<h2>Изменения, которые требуют решения</h2>\\n<div class=\"table-scroll\"><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>Сравнить старую и новую схему и найти чтения поля</td><td>Сохранить поле или выпустить новую версию</td></tr><tr><td>Клиент попадает в ветку «неизвестное состояние»</td><td>Enum расширили без обработки нового значения</td><td>Прогнать старый switch на каждом значении</td><td>Добавить обработку либо не включать значение в старый контракт</td></tr><tr><td>Запросы начинают отклоняться после обновления</td><td>Новое поле объявили обязательным</td><td>Отправить старую форму без поля</td><td>Сделать поле необязательным или изменить версию</td></tr><tr><td>Клиент принимает ответ, но действует по неверной ветке</td><td>Сохранили тип, но изменили смысл значения</td><td>Проверить примеры поведения, а не только JSON Schema</td><td>Сохранить семантику или переименовать поле</td></tr><tr><td>Ответ формально верен, но операция получает отказ</td><td>Нарушено право или текущее состояние ресурса</td><td>Проверить авторизацию и условие версии отдельно</td><td>Вернуть точный 403/409 и не маскировать его под 400</td></tr></tbody></table></div>\\n<p>Добавление необязательного поля чаще всего совместимо: старый клиент его игнорирует. Но это правило действует только для потребителя, который действительно игнорирует неизвестные свойства. У строгого декодера или схемы с запретом дополнительных полей появится отказ. Поэтому решение принимают по реальным правилам клиента, а не по названию изменения.</p>\\n<h2>Учебный валидатор на границе</h2>\\n<p>Следующая функция показывает минимальную проверку ответа. Она не ходит в сеть и не читает базу. Входом служит уже разобранный JavaScript-объект. Функция принимает только известную форму и возвращает нормализованное значение. Это учебный пример: он показывает границу контракта, но не заменяет OpenAPI, JSON Schema, интеграционный тест или авторизацию.</p>\\n<pre><code>function validateCustomerResponse(payload) {\\n if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {\\n return { ok: false, reason: 'body-must-be-object' };\\n }\\n\\n if (typeof payload.id !== 'string' || payload.id.length === 0) {\\n return { ok: false, reason: 'id-must-be-non-empty-string' };\\n }\\n\\n if (!Number.isInteger(payload.revision) || payload.revision < 1) {\\n return { ok: false, reason: 'revision-must-be-positive-integer' };\\n }\\n\\n if (!['active', 'blocked'].includes(payload.state)) {\\n return { ok: false, reason: 'state-is-outside-enum' };\\n }\\n\\n return {\\n ok: true,\\n value: { id: payload.id, revision: payload.revision, state: payload.state },\\n };\\n}\\n\\nconst accepted = validateCustomerResponse({\\n id: 'customer-17', revision: 4, state: 'active',\\n});\\nconst rejected = validateCustomerResponse({\\n id: 'customer-17', revision: 4, state: 'deleted',\\n});\\n\\nconsole.log(accepted.ok, accepted.value.state);\\nconsole.log(rejected.ok, rejected.reason);\\n// true active\\n// false state-is-outside-enum</code></pre>\\n<p>Отдельный отрицательный пример важнее ещё одного успешного fixture. Если сервер начнёт отправлять <code>state: deleted</code>, валидатор обнаружит изменение до того, как клиент выполнит неверную ветку. Если сервер отправит <code>revision: "4"</code>, отказ произойдёт по типу. Если поле исчезнет, причина должна назвать поле, а не скрыться за общим сообщением <code>invalid response</code>.</p>\\n<figure><img src=\"/assets/editorial/2027/mentor-series-2027-skill-map-contract.svg\" alt=\"Схема проверки API-контракта: ответ проходит проверку формы, затем проверку потребителя и только после этого используется клиентом.\" loading=\"lazy\" /><figcaption>Граница совместимости состоит из трёх проверок: форма ответа, поведение потребителя и условия операции. Схема не доказывает наличие права доступа или актуальность данных.</figcaption></figure>\\n<h2>Порядок проверки перед изменением</h2>\\n<ol><li>Назовите endpoint, метод, статус и media type. Отделите тело ответа от заголовков, запроса и внутренней модели.</li><li>Снимите текущую форму: обязательные поля, типы, nullable, enum и значения по умолчанию. Сохраните один успешный и несколько отрицательных примеров.</li><li>Найдите потребителей. Проверьте чтение полей, ветвления по enum, строгие декодеры и преобразователи DTO. Один найденный клиент не доказывает, что найден каждый.</li><li>Сравните старую и новую форму. Отдельно отметьте удаление поля, изменение типа, сужение enum и появление обязательного свойства.</li><li>Запустите runtime-валидатор на старом и новом ответе. Ошибка должна указывать путь к полю и причину отказа.</li><li>Прогоните consumer contract test со старым клиентом. Проверяйте не только десериализацию, но и ветку поведения для каждого допустимого значения.</li><li>Проверьте отрицательный путь: неизвестное поле, пропущенное поле, неверный тип, неизвестное enum-значение, 403 и конфликт версии. Для каждого случая зафиксируйте ожидаемый статус.</li><li>Если изменение несовместимо, выберите действие: сохранить старое поле, добавить новое рядом, открыть период deprecated или выпустить новую версию. Запишите условие удаления.</li></ol>\\n<h2>Где заканчивается JSON Schema</h2>\\n<p>Схема хорошо описывает типы, обязательность и ограничения документа. Она может запретить лишние поля или определить ветвление по значению. Но она не видит пользователя, базу и время. Ответ <code>state: active</code> может быть синтаксически правильным, хотя запись уже заблокирована. Значение <code>revision: 4</code> не доказывает, что обновление с ревизией 3 ещё допустимо.</p>\\n<p>Состояние требует отдельного протокола. Для конкурентного обновления подойдут версия ресурса и условный запрос с <code>If-Match</code>; для права — проверка роли до изменения; для отсутствующего ресурса — договорённый статус 404. Не превращайте 409 в 400: клиенту нужен сигнал, что запрос сформирован правильно, но состояние изменилось. Не повторяйте 403 автоматически: повтор не добавит прав.</p>\\n<p>Есть и отрицательный путь для самой проверки. Схема может пройти, а реальный handler — вернуть другой ответ в исключении. Поэтому contract test должен вызвать маршрут, проверить статус, заголовок и тело. Runtime-проверка должна работать на фактическом ответе, а не только на вручную собранном объекте. Это снижает конкретный риск, но не доказывает, что список потребителей полон.</p>\\n<h2>Ограничения и критерий готовности</h2>\\n<p>Учебная функция не проверяет OpenAPI-документ, сериализацию фреймворка, авторизацию, транзакции, сетевые сбои и содержимое базы. Она также не решает вопрос обратной совместимости для неизвестного клиента. Эти границы нужно оставить видимыми, иначе локальный зелёный тест создаст ложное чувство безопасности.</p>\\n<p>Изменение готово к выпуску, если команда может показать четыре доказательства: новая форма проходит schema- и runtime-проверку; старый клиент проходит consumer contract test; отрицательные случаи возвращают согласованные статусы и причины; для breaking change указаны версия, период совместимости и проверяемое условие удаления. Если хотя бы одного доказательства нет, результат следует считать непроверенным, даже когда сборка завершилась успешно.</p>\\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://spec.openapis.org/oas/v3.2.0.html\" target=\"_blank\" rel=\"noopener noreferrer\">OpenAPI Specification 3.2.0</a> — официальный формат описания HTTP-интерфейсов и операций.</li><li><a href=\"https://json-schema.org/draft/2020-12/json-schema-core.html\" target=\"_blank\" rel=\"noopener noreferrer\">JSON Schema Core 2020-12</a> — официальная спецификация языка схем для JSON-документов.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110: HTTP Semantics</a> — стандарт IETF для методов, статусов и семантики HTTP.</li></ul>"
|
||
}
|