{ "index": 70, "slug": "editorial-2026-01-field-platform-api", "title": "Как проверить совместимость платформенного API до изменения контракта", "excerpt": "Новое поле и номер версии не доказывают совместимость. Разбираем, как сопоставить контракт с конкретным потребителем, остановить несопоставимый путь и передать проверяемый результат.", "contentHtml": "

После небольшого изменения API клиент начинает показывать пустой экран. Платформенная команда добавила поле в JSON, оставила старые поля и подняла версию с 1.2.0 до 1.3.0. Один ручной запрос вернул правильный ответ. Через час другой клиент получает новый статус, не находит запись и повторяет запрос.

\n

Цена ошибки выше, чем неудачный запрос. Клиент может сохранить неверное состояние, повторить команду или показать пользователю, что объект исчез. Команда платформы тратит время на спор о слове «совместимо», хотя не записала, что именно клиент обязан прочитать и какие ошибки должен различать.

\n

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

\n

Сначала отделите форму ответа от его смысла

\n

Возьмём учебный API чтения каталожной записи. Он принимает recordId и возвращает обязательные поля id и state. Поле label необязательно. Ошибка fixed-not-found означает только отсутствие записи. Она не означает ошибку сети, отказ в доступе или невалидный запрос.

\n
GET /records/42 HTTP/1.1\nAccept: application/json\n\nHTTP/1.1 200 OK\nContent-Type: application/json\n\n{\n  \"id\": \"42\",\n  \"state\": \"active\",\n  \"label\": \"Учебная запись\"\n}
\n

Этот фрагмент показывает одну representation — передаваемую форму ресурса. Он не обещает порядок ключей, время ответа, сохранность записи или наличие поля в следующей версии. Даже наличие label в примере не делает его обязательным. Эти свойства нужно вынести в контракт явно. Иначе наблюдение быстро превращается в неофициальную гарантию.

\n

Потребитель должен быть описан так же точно. Например, fixed-tolerant-reader-v1 читает только id и state, принимает версию 1.3.0 и знает ошибку fixed-not-found. Другой клиент требует legacyMode. Для него тот же ответ неполон. Третий адаптер отправляет команды, а не читает записи. Его нельзя сравнивать с read API только из-за одинакового JSON.

\n

Минимальная карточка потребителя

\n

Не начинайте с перечня всех команд и репозиториев. Запишите минимальное решение, которое клиент принимает по ответу. В карточке нужны четыре поля: contractFamily, поддерживаемая версия, обязательные поля и известные ошибки. Если клиент зависит от порядка, повторов или специального заголовка, это тоже явное требование. Если требование неизвестно, статус должен остаться неизвестным.

\n
\"Цикл
Проверка сначала устанавливает сопоставимость, затем сравнивает поверхность и гарантии. Она не выпускает версию и не меняет API.
\n
Диагностика перед изменением контракта
СимптомПричинаПроверкаДействие
Клиент не видит новое полеПоле добавили, но клиент использует закрытую десериализациюСверить required fields и обработку unknown fieldsСохранить старый ответ или выпустить отдельный контракт
404 стал «нет записи» для всех ошибокКлиент смешал прикладную ошибку с сетевойВоспроизвести 404, 401, 403, 422 и timeout раздельноОставить fallback только для документированной ошибки
Похожий endpoint объявили несовместимымСравнили разные contract familyПроверить operation и family до сравнения полейВернуть stop-incomparable-consumer
После minor-версии изменился смысл статусаНовый символ или переход не вошёл в гарантиюСверить список значений и переходы состоянийОформить изменение как новый контракт или сохранить семантику
Ручной запрос успешен, релиз сломанПроверили один пример вместо named consumerЗапустить проверку на карточке конкретного клиентаНе передавать общий verdict без причин и next action
\n

Проверяйте family до полей

\n

Contract family — это вид операции и её смысловая граница. Read API, командный адаптер и webhook могут иметь поля id и state, но описывают разные действия. Сначала сравните fixed-catalog-read-v1 с тем, что объявил consumer. Если consumer относится к fixed-catalog-command-v1, проверка не должна доходить до полей.

\n

Такой результат не равен incompatibility. Команды пока не доказали, что объекты сопоставимы. Если назвать его просто «несовместимо», следующая команда начнёт ненужную миграцию. Точный статус сохраняет границу: нужен отдельный review для command family.

\n

Синтетический пример отрицательного пути

\n

Следующий код ограничен учебными объектами в памяти. Он не вызывает API, не читает production-трассы и не доказывает поведение реального клиента. Его задача — показать порядок решения: family проверяется раньше полей.

\n
const api = {\n  family: 'fixed-catalog-read-v1',\n  version: '1.3.0',\n  response: ['id', 'state'],\n  errors: ['fixed-not-found']\n};\n\nconst consumer = {\n  id: 'fixed-command-adapter-v1',\n  family: 'fixed-catalog-command-v1',\n  required: ['id', 'state']\n};\n\nfunction review(api, consumer) {\n  if (api.family !== consumer.family) {\n    return {\n      status: 'stop-incomparable-consumer',\n      nextAction: 'separate-contract-review-by-family'\n    };\n  }\n\n  const missing = consumer.required.filter(\n    (field) => !api.response.includes(field)\n  );\n  return missing.length\n    ? { status: 'incompatible-consumer', missing }\n    : { status: 'compatible-for-this-check' };\n}\n\nconsole.log(review(api, consumer));\n// { status: 'stop-incomparable-consumer',\n//   nextAction: 'separate-contract-review-by-family' }
\n

Положительный статус здесь тоже узкий. Он означает только, что зафиксированная пара прошла перечисленные проверки. Он не означает, что rollout безопасен, SLA выполнен или в системе нет неизвестных клиентов. В production-коде такой verdict должен сопровождаться именем consumer, версией и перечнем проверенных гарантий.

\n

Не путайте optional с совместимостью

\n

Слово optional обычно относится к конкретному валидатору или схеме. Оно не отвечает на вопрос, что сделает consumer, если поле отсутствует или появилось неожиданное поле. Один reader игнорирует расширение. Другой использует строгую модель. Третий считает отсутствие поля признаком старого режима.

\n

Предположим, что в ответ добавили legacyMode. Если tolerant reader его не использует, добавление может быть безопасным для этой пары. Но клиент, который требует поле, уже нельзя пометить compatible. Нельзя выводить обратное поведение из названия поля или из того, что ручной запрос всё ещё проходит.

\n

То же относится к значениям перечисления. Старый клиент может принимать active и archived, но падать на новом paused. В схеме поле осталось строкой, а смысл ответа изменился. Значит, проверка должна сравнивать не только наличие поля, но и допустимые значения и переходы, которые видит клиент.

\n

Отдельно фиксируйте ошибки и исключения

\n

Список ошибок — часть поведения consumer. Если fallback разрешён только для fixed-not-found, клиент не должен подставлять пустое состояние после 401, 403, 422 или таймаута. Иначе временный сбой превращается в потерю данных на экране, а повтор может отправить команду дважды.

\n

Иногда нужен специальный путь. Например, один внутренний инструмент получает расширенное представление. Это допустимо только как отдельный, названный и документированный контракт. Он должен описать получателя, версию, входные условия и то, чего не гарантирует. Секретный query-параметр вроде ?debug=1 не является исключением. Его обнаружит следующий consumer, но не обнаружит общий review.

\n

Если команда добавляет гарантию о стабильном порядке, времени ответа или доступности, её нельзя прятать рядом с полем. Это новая публичная обязанность. Её нужно назвать, проверить на соответствующем уровне и привязать к consumer. Один успешный trace не доказывает ни одну из этих гарантий.

\n

Что даёт номер версии

\n

SemVer полезен после того, как команда определила public API. Он помогает назвать совместимые добавления и несовместимые изменения. Но строка 1.3.0 сама не отвечает, является ли новый статус допустимым, игнорирует ли клиент неизвестные поля и относится ли consumer к той же семье.

\n

OpenAPI снижает догадки о форме HTTP-интерфейса: путях, параметрах, запросах, ответах и схемах. Это необходимый слой описания. Но документ не знает скрытую ветку клиентского кода. RFC 9110 также не превращает representation в гарантию прикладной совместимости. Поэтому стандарты дают словарь и границы, а итог принимает проверка конкретной пары.

\n

Порядок действий перед изменением

\n
  1. Назовите операцию. Запишите один endpoint, действие, contract family и версию. Не смешивайте чтение, команду и webhook.
  2. Назовите consumer. Укажите систему или модуль, владельца проверки и supported versions. Не используйте «все клиенты» как идентификатор.
  3. Опишите минимальное чтение. Перечислите обязательные поля, допустимые значения, ошибки и требования к неизвестным полям.
  4. Отсечьте другую family. При различии семей верните stop-incomparable-consumer и откройте отдельный review.
  5. Сопоставьте поверхность. Найдите отсутствующие поля, новые значения и изменившиеся ошибки. Каждый пробел оставьте причиной, а не спрячьте под номером версии.
  6. Проверьте исключения. Для отдельного пути потребуйте имя, версию, получателя и отрицательную границу гарантий.
  7. Сформируйте hand-off. Передайте status, reasons и next action. Положительный статус не запускает rollout и не заменяет тесты потребителя.
\n

Ограничения и критерий готовности

\n

Такой review не обнаруживает неизвестные интеграции сам по себе. Он не заменяет контрактные тесты, нагрузочные проверки, security review, миграцию данных, SLA или план отката. Учебный код выше работает на фиксированных литералах. Он не сообщает production-результат и не подтверждает, что реальный клиент действительно описал все свои зависимости.

\n

Проверку можно считать готовой только для явно ограниченной пары. В отчёте есть одна contract family, одна версия, один named consumer, список required fields, допустимые ошибки, заявленные гарантии и итоговый status. Для compatible-for-this-check нет неописанного claim и не осталось неизвестного обязательства. Для любого stop указаны причина и следующий шаг. Если хотя бы одного элемента нет, слово «совместимо» преждевременно.

\n

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

" }