Суть Читай реальные фрагменты OpenAPI YAML, вердикт oasdiff и сгенерированный клиент, затем выбирай фикс с наибольшим рычагом, чтобы держать contract честным.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Проблемы контракта ловятся в файле spec, в выводе diff и в сгенерированном клиенте. Читай каждый фрагмент, предсказывай влияние на потребителей и выбирай фикс, который senior-инженер делает первым.
Цель
Отработай цикл, через который проходит каждое изменение API: читай схему, предсказывай последствия для breaking changes и codegen и тянись к фиксу, который держит spec несущим.
Фрагмент 1 — слабая схема
components: schemas: Order: type: object properties: id: { type: string } items: type: object # нетипизированный блоб total: {} # вообще без типа additionalProperties: true # любые другие ключи разрешены # нет массива `required:`
Викторина
Completed
Генератор превращает эту схему Order в клиент. Как будет выглядеть сгенерированный тип и каков фикс?
Heads-up Генераторы не выдумывают ограничения, которые схема опускает. Нетипизированный object становится any, отсутствующий массив required делает всё optional, а открытый additionalProperties запрещает закрытый тип. Вывод честно слабый.
Heads-up Эта схема — валидный OpenAPI: слабый, но легальный. Генерируется нормально, просто выдаёт слабый клиент с any, который сводит на нет смысл codegen.
Heads-up Валидировать не по чему — additionalProperties: true и нетипизированные поля принимают почти всё. Слабые схемы ослабляют и сгенерированные типы, и валидацию на краю.
Фрагмент 2 — вердикт oasdiff
$ oasdiff breaking main.yaml pr.yaml1 breaking changes: 1 error, 0 warningerror api-required-request-property-added in API POST /orders added the required request property 'tenantId'
Викторина
Completed
PR добавляет tenantId как required-поле в POST /orders, и oasdiff сообщает об этом в CI. Что должен сделать пайплайн и какова совместимая альтернатива?
Heads-up Добавление optional-поля совместимо; добавление required — хрестоматийный breaking change, потому oasdiff и классифицирует его как error, а не warning.
Heads-up Перегенерированные docs описывают изменение, но не предотвращают поломку. Вся ценность gate в том, что breaking change роняет сборку до того, как сломается клиент.
Heads-up Поломка в запросе, а не в ответе. Правка ответа не делает новое required-поле запроса совместимым.
Фрагмент 3 — переписывание nullability с 3.0 на 3.1
Ты мигрируешь spec на 3.1 и переписываешь nullability вот так. За чем надо следить в тулинге и дифах?
Heads-up 3.1 полностью убрал nullable; он игнорируется или невалиден. Нужно использовать type: [string, 'null'], а инструменты под 3.0 могут его не понять.
Heads-up Обе выражают 'строка или null' — контракт идентичен. Миграция синтаксическая; допустимые значения те же.
Heads-up type: [string, 'null'] — это объединение допустимых типов, не связанное с тем, required ли поле. Required-ность контролируется только массивом required.
Фрагмент 4 — code-first аннотации
class CreateOrder(BaseModel): id: str items: list[Item] coupon: str | None = None # optional, nullable# FastAPI генерирует OpenAPI spec из этой модели.# Позднее правка делает coupon required: coupon: str # теперь required, без дефолта
Викторина
Completed
В code-first-сетапе инженер делает coupon required, убрав его дефолт. Сгенерированный spec обновляется автоматически. В чём риск и что его закрывает?
Heads-up Code-first предотвращает drift между spec и хендлером, а не между контрактом и тем, на что опираются клиенты. Изменение required-поля — breaking, как бы spec ни был получен.
Heads-up Модель — валидный Python; FastAPI стартует нормально и честно публикует coupon как required. Опасность ниже по течению, на клиентах, а не на старте.
Heads-up Spec-first выносит изменение на review, но всё равно не блокирует его без diff-gate. Фикс — это enforcement (oasdiff в CI), который нужен обеим моделям одинаково.
Итог
Каждое изменение контракта читается в трёх местах: схема (слабые типы и открытый additionalProperties дают клиенты с any — ужесточай явными типами, массивами required и $ref), diff (oasdiff классифицирует добавление required-поля, удаление поля ответа или сужение типа как breaking — роняй PR и мигрируй совместимо) и направление генерации (code-first синхронен с кодом, но не с потребителями, поэтому ему нужен тот же gate, что и spec-first). OpenAPI 3.1 переносит nullability в type-массив и выравнивается с JSON Schema 2020-12 — проверь поддержку инструментов до миграции. Читай артефакт, предсказывай влияние на потребителя, затем чини на уровне контракта.