OpenAPI 3.1 vs 3.0: что реально поменялось и стоит ли мигрировать
OpenAPI 3.1 вышел в 2021, и до сих пор половина команд сидит на 3.0, потому что «там же всё работает». Работает, да. Только когда надо описать поле, которое одновременно строка и null, или сослаться на JSON Schema из внешнего файла, начинаются костыли. 3.1 решает большинство таких проблем, но миграция не бесплатна: tooling догоняет неравномерно, кодогенераторы ломаются на webhooks, и пара любимых хаков из 3.0 перестаёт работать.
Разберу по пунктам, что реально поменялось между версиями, где 3.1 лучше, а где можно подождать. Без «революционных улучшений» и «следующего поколения» — конкретные изменения и решение, мигрировать или нет.
Главное: 3.1 — это надмножество JSON Schema 2020-12
В 3.0 схема для тел запросов и ответов формально похожа на JSON Schema, но это собственный диалект OpenAPI с заметными отличиями. nullable: true, example в единственном числе, отсутствие const, ограниченная поддержка $ref. Из-за этого валидаторы JSON Schema не работали с OpenAPI-схемами напрямую — нужны были адаптеры.
В 3.1 схемы — это полноценный JSON Schema 2020-12. Что это значит на практике:
- Можно брать любую готовую JSON Schema (например, из
schemas.json-schema.org) и подставлять в OpenAPI без переделки. - Валидатор JSON Schema, который у тебя уже есть в коде, работает на тех же схемах, что и документация.
- Появились
const,if/then/else,$dynamicRef,unevaluatedProperties— то, что в 3.0 либо отсутствовало, либо называлось по-другому.
Nullable: было — стало
В 3.0 nullable-поле описывалось так:
properties:
middleName:
type: string
nullable: true
В 3.1 атрибут nullable убрали. Вместо него — массив типов, как в JSON Schema:
properties:
middleName:
type: [string, "null"]
Кажется мелочью, но «нул» в кавычках забывают постоянно. Без них YAML-парсер превратит значение в null, а не в строку "null", и валидация сломается с непрозрачной ошибкой. Лично для меня это самый частый баг при миграции.
Если у тебя в спеке десятки nullable: true, есть готовые скрипты — например, swagger2openapi с флагом --targetVersion 3.1.0 переписывает их автоматически.
Examples: множественное число
В 3.0 на уровне схемы было одно example, и его дублировали в examples на уровне media-type:
schema:
type: object
properties:
name:
type: string
example: Alice
В 3.1 на уровне схемы появилось examples (массив) — синхронно с JSON Schema:
schema:
type: object
properties:
name:
type: string
examples: [Alice, Bob]
Старое example в 3.1 deprecated, но работает. Если миграция через автоматический конвертер — он обычно оставляет example как есть, и в спеке начинают сосуществовать оба варианта. По возможности приведи к одному, иначе ревьюверы регулярно будут спорить, какой использовать в новых эндпоинтах.
Webhooks как отдельная сущность
В 3.0 описать webhook (когда твой сервис стучится в URL клиента) можно было только через callbacks внутри какого-то эндпоинта. Если webhook не привязан к конкретному запросу — например, «мы пушим события order.created, когда хотим» — приходилось придумывать фейковый эндпоинт «subscribe» только ради callback-блока.
В 3.1 webhooks — это корневая секция:
webhooks:
orderCreated:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/OrderEvent'
responses:
'200':
description: Webhook received
Этого достаточно, чтобы Stoplight/Redoc отрисовали отдельный раздел с webhook-эндпоинтами, а кодогенераторы — собрали серверную «ручку» для приёма пушей. До 3.1 я оформляла такие интеграции в отдельной markdown-странице, потому что в спеку нормально не клалось.
Files: $ref в любую сторону
В 3.0 разрешение $ref между файлами работало, но с оговорками: ссылаться можно было только на JSON Schema-объекты, и инструменты по-разному обрабатывали относительные пути. В 3.1 поведение $ref стандартизовано через JSON Reference 2020-12: можно ссылаться на любой узел любого файла, поддерживается $id для bundle.
paths:
/users:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: './schemas/user.yaml#/User'
Это удобно для команд, где спека разбита на десятки файлов: один путь, одна схема, один $ref. Bundle-инструменты (redocly bundle, swagger-cli bundle) собирают всё в один файл для деплоя.
Mutual exclusion: oneOf vs anyOf
В 3.0 различие oneOf/anyOf часто игнорировали — половина инструментов валидировала их одинаково. В 3.1 семантика жёсткая: oneOf требует совпадения ровно с одной схемой, anyOf — с одной или больше. Если у тебя был трюк «положу две похожие схемы в oneOf, авось пройдёт» — в 3.1 он перестанет работать на строгих валидаторах.
Mutual TLS и новые security-схемы
В 3.1 добавили mutualTLS как тип securityScheme:
components:
securitySchemes:
mtls:
type: mutualTLS
description: Client certificate required
В 3.0 mTLS описывали через комментарии или OpenID Connect — некрасиво. Если у тебя b2b-интеграции с сертификатами, это маленькая, но приятная мелочь.
Что сломается при миграции
Самые частые проблемы, на которые натыкаются команды:
- Кодогенераторы. На середину 2026
openapi-generatorподдерживает 3.1 не во всех target-языках одинаково. Проверь свой генератор до миграции, иначе после конвертации перестанет собираться SDK. - Mock-серверы. Prism, Stoplight Studio — поддержка 3.1 есть, но местами сырая. Особенно в части новых JSON Schema-конструкций (
unevaluatedProperties,$dynamicRef). - Type generators.
openapi-typescriptи аналоги — обычно ок, но проверь, чтоtype: [string, "null"]у них превращается вstring | null, а не вstring | "null". - Внутренние тулзы. Если у вас есть свой парсер спеки (для линтинга, для рендера админки), он почти наверняка завязан на 3.0 и не поймёт
webhooksили массив типов.
Поэтому миграция — это не swagger2openapi в CI и пинок в прод, а отдельный спринт с прогонкой всех зависимых инструментов.
Когда мигрировать прямо сейчас
Стоит, если:
- Описываешь webhooks и заметно страдаешь от их отсутствия в 3.0.
- В коде уже используется JSON Schema 2020-12 — тогда в 3.1 пропадает дублирование схем.
- API на этапе разработки, нет внешних клиентов, которые ломаются от изменений.
- В команде есть кто-то, кто прогонит зависимый tooling и почистит хвосты.
Когда лучше остаться на 3.0
Не торопись, если:
- SDK генерируются в Java/Go/.NET через старый кодогенератор и собираются в CI каждого релиза.
- Спека — публичный контракт со множеством клиентов, сторонних SDK, gateway-ями. Любой риск ломучести трогать дороже.
- Твой основной инструмент рендера/мок-сервера ещё не поддерживает 3.1 (бывает, особенно у внутренних форков SwaggerUI).
Рецепт миграции, если решились
- Сделай ветку, прогони
swagger2openapi --targetVersion 3.1.0на корневом файле. - Открой diff, прочитай каждое изменение глазами. Особенно — преобразование
nullableиexample/examples. - Прогоняй спеку через
spectral lintилиredocly lintс правилами для 3.1. - Сгенерируй SDK всеми генераторами, которые используешь. Собери, запусти тесты SDK.
- Прогоняй mock-сервер на тестовых запросах и проверь, что валидируется так, как ожидаешь.
- Отдельно проверь
oneOf/anyOf— если строгая валидация поломалась, чини схемы, а не возвращайся на 3.0. - Только после этого мерж в main и публикация документации.
Я обычно закладываю на такую миграцию неделю на средних спеках (50–100 эндпоинтов). На больших — две, потому что половина времени уходит не на саму спеку, а на смежные тулзы.
Что в 3.1 не поменялось
Чтобы не было ложных ожиданий: 3.1 не вводит новых способов описывать асинхронные API (это AsyncAPI), GraphQL, gRPC. Не появилось встроенной поддержки версионирования API. Структура paths/components/servers та же. Если у тебя проблема с одной из этих областей, версия OpenAPI не поможет — нужен другой инструмент или соглашение в команде.
Итог по решению
Если новый проект — пиши сразу на 3.1, никаких причин начинать на 3.0 в 2026 нет. Если зрелая спека с экосистемой клиентов — мигрируй, но запланированно: проверь tooling, выдели спринт, прогони regression-тесты SDK. Главное преимущество 3.1 — единая схема валидации для кода и доки, поэтому максимум выигрыша получают команды, которые уже используют JSON Schema в рантайме.