OpenAPI/Swagger, code generation, contract-first vs code-first, валидация#
Модуль: Backend · Уровень: Middle+/Senior
TL;DR#
- OpenAPI — машиночитаемая спецификация (контракт) HTTP/REST API в YAML/JSON. До версии 3.0 называлась Swagger (2.0 == “Swagger 2.0”). Swagger сейчас — это набор инструментов (Swagger UI, Swagger Editor, Codegen) от SmartBear, а сам формат — OpenAPI Specification (OAS) под управлением OpenAPI Initiative (Linux Foundation).
- Версии: 2.0 (2014, Swagger) → 3.0 (2017, переработана структура:
components,requestBody,servers) → 3.1 (2021, полная совместимость с JSON Schema Draft 2020-12). - Два подхода: contract-first (design-first) — сначала пишем спеку, из неё генерим типы/сервер/клиент (
oapi-codegen); code-first — пишем Go-код с аннотациями, из него генерим спеку (swaggo/swag). Для контракта между командами/фронтом промышленный стандарт — contract-first. - Спека = source of truth. Контракт между бэкендом, фронтом, мобайлом, внешними потребителями. Версионируется в git, линтуется в CI (
spectral,vacuum), на breaking changes проверяется (oasdiff). - Валидация: статическая (линт спеки в CI) + рантайм (middleware на базе
kin-openapiвалидирует request/response против схемы).
Простыми словами#
OpenAPI — машиночитаемое описание твоего HTTP API: какие есть эндпоинты, что они принимают и что возвращают.
Зачем это нужно на практике: из одного YAML-файла генерируются клиенты для фронтенда и мобильных приложений, серверные заготовки с типами, документация, а в CI спека проверяется на несовместимые изменения. Файл лежит в git и становится контрактом между командами — вместо переписки в мессенджере о том, стало поле обязательным или нет.
Два подхода. Contract-first: сначала пишем спеку, потом генерируем из неё Go-код (oapi-codegen). Code-first: пишем код с комментариями-аннотациями, из них генерируем спеку (swaggo). Когда API — контракт между несколькими командами, обычно побеждает первый: спеку можно обсудить до того, как написана хоть строка кода.
Про названия, чтобы не путаться: Swagger — старое имя формата (версии до 3.0) и заодно набор инструментов вокруг него. Сам формат сегодня называется OpenAPI.
Если ты с другого языка. Аналог WSDL из мира SOAP или .proto из gRPC, только описывает обычный REST поверх JSON. Если приходишь из Java со Spring, привычка «аннотации на контроллерах породят спеку» здесь тоже работает, но в Go чаще идут в обратную сторону.
Словарь этой статьи:
- спецификация (спека) — тот самый YAML или JSON файл с описанием API;
- кодогенерация — получение типов, клиента или каркаса сервера прямо из спеки;
- breaking change — изменение, ломающее существующих клиентов: удалили поле, сделали необязательное обязательным, поменяли тип;
- линтер спеки (
spectral,vacuum) — проверяет спеку на единообразие и типовые ошибки в CI.
Теория#
Что такое OpenAPI#
OpenAPI Specification (OAS) — формальное, языконезависимое описание HTTP API. Описывает: эндпоинты, методы, параметры, тела запросов/ответов, схемы данных, аутентификацию, примеры. Главная ценность — единый машиночитаемый контракт, из которого можно:
- генерировать серверные стабы и клиентов (десятки языков);
- генерировать документацию (Swagger UI, Redoc, Stoplight);
- валидировать трафик в рантайме;
- проверять обратную совместимость;
- использовать в mock-серверах (Prism) для параллельной разработки фронта и бэка.
История названий:
| Эра | Название формата | Кто ведёт |
|---|---|---|
| 2010–2015 | Swagger Specification | SmartBear |
| 2016+ | OpenAPI Specification (OAS) | OpenAPI Initiative (Linux Foundation) |
«Swagger 2.0» и «OpenAPI 2.0» — синонимы. С 3.0 используют только «OpenAPI».
Версии: 2.0 vs 3.0 vs 3.1#
| Аспект | OpenAPI 2.0 (Swagger) | OpenAPI 3.0 | OpenAPI 3.1 |
|---|---|---|---|
| Корневой ключ | swagger: "2.0" | openapi: 3.0.x | openapi: 3.1.x |
| Серверы | host + basePath + schemes | servers: [] (URL-шаблоны, переменные) | то же |
| Тело запроса | parameters с in: body | отдельный requestBody | то же |
| Переиспользуемые объекты | definitions, parameters, responses | единый components | то же |
| Несколько content-type | один на operation | content: {application/json: ..., ...} | то же |
| JSON Schema | подмножество Draft 4 (свои отклонения) | расширенное подмножество Draft 5/«Wright» | полный JSON Schema 2020-12 |
nullable | нет (хаки через x-nullable) | nullable: true | нет nullable; вместо него type: [string, "null"] (массив типов) |
type как массив | нет | нет | да (type: ["string","null"]) |
| Webhooks | нет | нет | да (webhooks) |
examples (множ.) | нет | да | да |
Произвольный $schema/$id | нет | ограниченно | да |
Ключевой сдвиг в 3.1 — это переход на JSON Schema 2020-12. Это значит, что схемы из OpenAPI 3.1 можно переиспользовать в обычных JSON Schema валидаторах и наоборот. Но это создаёт боль в тулинге: многие генераторы и валидаторы заточены под 3.0 и не понимают type: ["string","null"] или nullable-в-3.1-нет. На 2026 год часть production-инструментов всё ещё лучше работает с 3.0.x, поэтому версию выбирают исходя из зрелости тулчейна команды.
Структура спецификации#
Корневые объекты OpenAPI 3.x:
| Поле | Назначение |
|---|---|
openapi | версия спеки (строка, напр. 3.0.3) |
info | метаданные: title, version (версия API, не спеки), description, contact, license |
servers | список базовых URL (с переменными окружения) |
paths | объект «путь → операции» (get, post, put, patch, delete, …) |
components | переиспользуемые объекты: schemas, parameters, requestBodies, responses, securitySchemes, headers, examples |
security | глобальные требования безопасности (ссылки на securitySchemes) |
tags | группировка операций для документации |
Внутри operation: operationId (уникальный, критичен для кодогенерации — из него генерится имя метода), summary, parameters (path/query/header/cookie), requestBody, responses, security, tags.
parameters — каждый имеет name, in (path|query|header|cookie), required, schema. Для in: path всегда required: true.
requestBody — content с разбивкой по media-type, каждый со своей schema.
responses — ключи это HTTP-коды (200, 404, default), внутри description (обязателен!) + content + headers.
securitySchemes — типы: apiKey, http (basic/bearer), oauth2, openIdConnect, mutualTLS (3.1).
Пример YAML-спеки для CRUD-эндпоинта#
openapi: 3.0.3
info:
title: Users API
version: 1.2.0
description: CRUD over users
servers:
- url: https://api.example.com/v1
description: production
- url: http://localhost:8080/v1
description: local
security:
- bearerAuth: []
paths:
/users:
get:
operationId: listUsers
tags: [users]
summary: List users
parameters:
- name: limit
in: query
required: false
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
- name: cursor
in: query
schema: { type: string }
responses:
'200':
description: page of users
content:
application/json:
schema:
$ref: '#/components/schemas/UserPage'
'401':
$ref: '#/components/responses/Unauthorized'
post:
operationId: createUser
tags: [users]
summary: Create user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: created
headers:
Location:
schema: { type: string, format: uri }
content:
application/json:
schema: { $ref: '#/components/schemas/User' }
'400':
$ref: '#/components/responses/BadRequest'
'409':
description: email already exists
/users/{id}:
parameters:
- name: id
in: path
required: true
schema: { type: string, format: uuid }
get:
operationId: getUser
tags: [users]
responses:
'200':
description: user
content:
application/json:
schema: { $ref: '#/components/schemas/User' }
'404':
$ref: '#/components/responses/NotFound'
patch:
operationId: updateUser
tags: [users]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/UpdateUserRequest' }
responses:
'200':
description: updated
content:
application/json:
schema: { $ref: '#/components/schemas/User' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
operationId: deleteUser
tags: [users]
responses:
'204':
description: deleted
'404': { $ref: '#/components/responses/NotFound' }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
responses:
BadRequest:
description: validation error
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
Unauthorized:
description: missing/invalid token
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
NotFound:
description: resource not found
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
schemas:
User:
type: object
required: [id, email, createdAt]
properties:
id: { type: string, format: uuid }
email: { type: string, format: email }
name: { type: string, maxLength: 255 }
createdAt: { type: string, format: date-time }
CreateUserRequest:
type: object
required: [email]
properties:
email: { type: string, format: email }
name: { type: string, maxLength: 255 }
UpdateUserRequest:
type: object
properties:
name: { type: string, maxLength: 255 }
UserPage:
type: object
required: [items]
properties:
items:
type: array
items: { $ref: '#/components/schemas/User' }
nextCursor: { type: string }
Error:
type: object
required: [code, message]
properties:
code: { type: string }
message: { type: string }
details:
type: array
items: { type: string }Contract-first vs code-first#
Contract-first (design-first): спека — артефакт, который пишут (часто фронт + бэк вместе) до кода. Из неё генерируют типы, server interface, клиентов. Спека ревьюится в PR как любой код.
Code-first: истина — Go-код с аннотациями (swaggo/swag) или со структурными тегами; спека генерируется из кода как побочный продукт.
| Критерий | Contract-first | Code-first |
|---|---|---|
| Source of truth | YAML-спека | Go-код / аннотации |
| Параллельная работа фронт/бэк | отлично (mock из спеки сразу) | плохо (нужен код) |
| Риск дрейфа кода и спеки | низкий (код генерится из спеки) | высокий (аннотации забывают обновить) |
| Гарантия валидности спеки | высокая | спека может не валидироваться, генерится «как есть» |
| Барьер входа | выше (надо знать OAS) | ниже (пишешь привычный Go) |
| Контроль над сгенерированным кодом | средний | полный (код твой) |
| Кросс-командный контракт | сильная сторона | слабая |
| Документация | первична | вторична, легко отстаёт |
| Типичный инструмент в Go | oapi-codegen | swaggo/swag |
Senior-выбор: для публичного/межкомандного API — contract-first. Для внутреннего сервиса, который никто кроме тебя не дёргает, и где спека нужна лишь для доки — допустим code-first, но осознавая риск дрейфа.
Кодогенерация в Go#
oapi-codegen (contract-first)#
oapi-codegen (форк/наследник deepmap/oapi-codegen, сейчас под oapi-codegen/oapi-codegen) генерирует из OpenAPI 3.x:
- типы (Go-структуры из
components/schemas); - server interface под нужный роутер:
chi,echo,gin,net/http(std-http),fiber,gorilla; - HTTP-клиент (типизированные методы);
- встроенную спеку (
embedded-spec) для рантайм-валидации; - strict server — обёртка, где хендлеры принимают типизированный request-объект и возвращают типизированный response (роутер сам парсит/сериализует).
Конфиг (oapi-codegen.yaml):
# oapi-codegen.yaml
package: api
output: internal/api/api.gen.go
generate:
models: true
chi-server: true # сгенерировать ServerInterface под chi
strict-server: true # строгий типизированный слой поверх
embedded-spec: true
output-options:
skip-prune: false
user-templates: {} # можно подменять шаблоныЗапуск через go generate:
//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=oapi-codegen.yaml openapi.yamlСгенерированный ServerInterface (упрощённо, для chi):
// api.gen.go (фрагмент)
type ServerInterface interface {
// (GET /users)
ListUsers(w http.ResponseWriter, r *http.Request, params ListUsersParams)
// (POST /users)
CreateUser(w http.ResponseWriter, r *http.Request)
// (GET /users/{id})
GetUser(w http.ResponseWriter, r *http.Request, id openapi_types.UUID)
// (PATCH /users/{id})
UpdateUser(w http.ResponseWriter, r *http.Request, id openapi_types.UUID)
// (DELETE /users/{id})
DeleteUser(w http.ResponseWriter, r *http.Request, id openapi_types.UUID)
}
type ListUsersParams struct {
Limit *int `form:"limit,omitempty"`
Cursor *string `form:"cursor,omitempty"`
}
// Регистрирует роуты в chi.Router и вешает обработчики на ServerInterface.
func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler { /* ... */ }Со strict-server интерфейс становится чище — без http.ResponseWriter:
type StrictServerInterface interface {
ListUsers(ctx context.Context, request ListUsersRequestObject) (ListUsersResponseObject, error)
CreateUser(ctx context.Context, request CreateUserRequestObject) (CreateUserResponseObject, error)
GetUser(ctx context.Context, request GetUserRequestObject) (GetUserResponseObject, error)
// ...
}
// Имплементация:
func (s *Service) CreateUser(ctx context.Context, req CreateUserRequestObject) (CreateUserResponseObject, error) {
u, err := s.users.Create(ctx, req.Body.Email, deref(req.Body.Name))
if err != nil {
if errors.Is(err, ErrEmailTaken) {
return CreateUser409JSONResponse{Error{Code: "conflict", Message: "email exists"}}, nil
}
return nil, err // 500
}
return CreateUser201JSONResponse(toAPIUser(u)), nil
}Тонкости oapi-codegen:
operationIdобязателен и должен быть уникальным — иначе имена методов конфликтуют/мусорные.- Управление маппингом типов:
x-go-type,x-go-type-importдля подмены сгенерированного типа на свой (напр. кастомныйMoney,decimal.Decimal). nullablevsrequiredопределяет, генерится поле как*TилиT. Отсутствиеrequired→ указатель/omitempty.additionalProperties: true→map[string]interface{}(или генерится спец-тип сAdditionalProperties).oneOf/anyOfгенерируются в union-тип с методамиAs.../Merge...— работать с ними неудобно, на senior это частый разговор о моделировании.
swaggo/swag (code-first)#
Генерирует Swagger 2.0 (не 3.x!) из аннотаций в комментариях Go. Это его принципиальное ограничение — для 3.x придётся конвертировать.
// @Summary Create user
// @Tags users
// @Accept json
// @Produce json
// @Param body body CreateUserRequest true "payload"
// @Success 201 {object} User
// @Failure 400 {object} Error
// @Router /users [post]
func (h *Handler) CreateUser(c *gin.Context) { /* ... */ }swag init парсит AST и комментарии → docs/swagger.json + swagger.yaml + Go-пакет docs. Минусы: спека вторична, аннотации легко рассинхронизировать с реальным поведением, ограничены Swagger 2.0, нет строгой типобезопасности контракта.
Сравнение генераторов#
| Инструмент | Подход | OAS-версия | Что генерит |
|---|---|---|---|
oapi-codegen | contract-first | 3.0/3.1 | типы, server iface, client, strict-server, embedded spec |
swaggo/swag | code-first | 2.0 | спека из аннотаций + Swagger UI |
ogen | contract-first | 3.0/3.1 | типы + сервер + клиент, без рефлексии, встроенная валидация, OTEL |
go-swagger | оба | 2.0 | сервер/клиент (тяжёлый, legacy на 2.0) |
deepmap/oapi-codegen — старое имя; проект переехал в org oapi-codegen, импорт-путь github.com/oapi-codegen/oapi-codegen/v2. На собеседовании упоминание «deepmap» — маркер того, что человек давно с ним работает.
Валидация#
Два уровня:
1. Статическая (CI, линт спеки):
- Spectral (Stoplight) — линтер OpenAPI/AsyncAPI/JSON Schema с настраиваемыми правилами (
.spectral.yaml): обязательныеoperationId, описания, запретadditionalPropertiesбез схемы, нейминг-конвенции и т.д. - vacuum — быстрый (Go) линтер, совместим с правилами Spectral, удобен для больших спек в CI.
# .spectral.yaml
extends: ["spectral:oas"]
rules:
operation-operationId: error
operation-description: warn
oas3-unused-component: warn2. Рантайм (request/response validation middleware):
kin-openapi (getkin/kin-openapi) — Go-библиотека: парсит спеку (openapi3.Loader), строит роутер (gorillamux/legacy), валидирует входящие запросы и (опционально) исходящие ответы против схемы.
loader := openapi3.NewLoader()
doc, _ := loader.LoadFromFile("openapi.yaml")
_ = doc.Validate(loader.Context) // проверка самой спеки
router, _ := gorillamux.NewRouter(doc)
func ValidatorMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
route, pathParams, err := router.FindRoute(r)
if err != nil { http.Error(w, "not found in spec", 404); return }
reqInput := &openapi3filter.RequestValidationInput{
Request: r, PathParams: pathParams, Route: route,
}
if err := openapi3filter.ValidateRequest(r.Context(), reqInput); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest); return
}
next.ServeHTTP(w, r)
})
}oapi-codegen интегрируется с kin-openapi: генерируемый embedded-spec + middleware oapimiddleware.OapiRequestValidator(swagger) даёт авто-валидацию запросов против встроенной спеки.
Response-валидация обычно включают только в тестах/staging (на проде дорого и рискованно ронять валидный для клиента ответ из-за расхождения схемы).
Source of truth и кросс-командное взаимодействие#
Спека — контракт. Практики senior-уровня:
- Спека живёт в git (отдельный репо
api-specsили рядом с сервисом), ревьюится в PR. - Из неё генерится: серверный код бэка, типизированные клиенты (фронт через
openapi-typescript/orval, другие сервисы черезoapi-codegen). - Mock-сервер (Prism,
stoplight/prism) поднимается из спеки — фронт пилит UI, не дожидаясь бэка. - Документация (Redoc/Swagger UI) генерится автоматически — всегда актуальна.
- Single source of truth убирает класс багов «фронт ждёт одно, бэк отдаёт другое».
Версионирование и breaking changes#
Версионирование API (не путать с версией спеки openapi:):
- В пути:
/v1/users,/v2/users— самый явный, грубый. - В заголовке:
Accept: application/vnd.api+json; version=2или кастомныйX-API-Version. - Поле
info.version(semver) — версия документа/API.
Breaking vs non-breaking:
| Non-breaking (backward compatible) | Breaking |
|---|---|
| добавление нового optional-поля в ответ | удаление поля из ответа |
| новый эндпоинт | удаление/переименование эндпоинта |
| новый optional query-параметр | новый required параметр |
| расширение enum в ответе (спорно для клиентов) | сужение enum, смена типа поля |
| ослабление валидации входа | ужесточение валидации входа, новый required в body |
oasdiff — инструмент для diff двух версий спеки и детекта breaking changes; ставят в CI как gate на PR:
oasdiff breaking old.yaml new.yaml --fail-on ERR
# выводит классифицированные изменения; ненулевой код → блокируем mergeТак контракт защищён: нельзя случайно сломать потребителей. Для намеренных breaking changes — бамп мажорной версии API и план миграции (deprecation через deprecated: true + Sunset/Deprecation заголовки).
Подводные камни / gotchas#
- 3.1 ≠ просто «новее»: переход на JSON Schema 2020-12 ломает совместимость с тулингом, заточенным под 3.0.
nullableв 3.1 нет — вместо негоtype: ["string","null"]. Многие генераторы/UI на 2026 ещё неполно поддерживают 3.1. swaggo/swagотдаёт Swagger 2.0, а не OpenAPI 3.x. Если в требованиях «OpenAPI 3», то либоoapi-codegen/ogen(contract-first), либо конвертация 2.0→3.0 (теряются нюансы).- Дрейф code-first: аннотации забывают обновить — спека и код расходятся, документация врёт. В contract-first код генерится, дрейф невозможен (но можно забыть перегенерить — добавляют CI-проверку «generated up to date»).
operationIdколлизии/отсутствие → уoapi-codegenмусорные/конфликтующие имена методов. Делать его обязательным правилом Spectral.required≠ nullable.requiredговорит «поле присутствует», nullable — «значение может быть null». В Go:required && !nullable→T; иначе →*T/omitempty. Путаница ведёт к багам сериализации (отправкаnullvs отсутствие ключа).additionalProperties: по умолчанию в JSON Schema разрешены любые доп. поля. Если хочешь strict — явноadditionalProperties: false. Иначе валидация пропустит мусор; в Go генеритсяmap, теряется типобезопасность.oneOf/anyOf/allOf— мощно в схеме, но в Go генерируются в неудобные union-типы.allOfчасто (неправильно) используют как «наследование/композицию»; для дискриминируемых union нуженdiscriminator.- Response-валидация в рантайме на проде может уронить корректный для клиента ответ из-за расхождения схемы. Держать в тестах/staging.
format(email,uuid,date-time) — это аннотация; не все валидаторы его проверяют, и не все генераторы мапят в нужный Go-тип. Проверять, что выбранный тулчейн действительно валидирует format.$refи циклические ссылки: глубокие/циклические$refломают часть генераторов; внешние$ref(на другие файлы/URL) поддерживаются неравномерно — часто бандлят в один файл (redocly bundle).- Версия спеки vs версия API:
openapi: 3.0.3— версия формата;info.version— версия твоего API. Их регулярно путают. - embedded-spec и расхождение: если валидируешь по встроенной (на момент сборки) спеке, а деплоят другую — поведение разойдётся; держать единый источник.
Вопросы на собеседовании#
В: В чём разница между Swagger и OpenAPI? О: Swagger — историческое название формата (версия 2.0 == «Swagger 2.0») и набор инструментов SmartBear (Swagger UI/Editor/Codegen). С версии 3.0 формат называется OpenAPI Specification и ведётся OpenAPI Initiative под Linux Foundation. То есть OpenAPI — это спецификация, Swagger сегодня — тулинг вокруг неё.
В: Что принципиально нового в OpenAPI 3.1 по сравнению с 3.0?
О: Полная совместимость с JSON Schema Draft 2020-12. Исчез nullable (теперь type: ["string","null"]), type может быть массивом, появились webhooks, поддержка $schema/$id, mutualTLS. Минус — часть тулинга ещё не догнала 3.1, поэтому переход не бесплатный.
В: Contract-first vs code-first — что выберешь для API между несколькими командами и почему? О: Contract-first. Спека — единый source of truth, ревьюится отдельно, из неё генерится сервер и типизированные клиенты для всех потребителей, можно сразу поднять mock-сервер для параллельной разработки фронта. Code-first (swaggo) удобен для внутреннего сервиса, но спека вторична и склонна к дрейфу с кодом.
В: Что генерирует oapi-codegen и чем strict-server отличается от обычного?
О: Генерирует Go-типы из components/schemas, ServerInterface под выбранный роутер (chi/echo/gin/std-http), HTTP-клиент, опционально embedded-spec. Обычный server interface даёт методы с http.ResponseWriter/*http.Request — парсинг/сериализация на тебе. Strict-server генерирует слой, где хендлер принимает типизированный RequestObject и возвращает типизированный ResponseObject (напр. CreateUser201JSONResponse), а инфраструктурный код сам парсит вход и сериализует выход — меньше boilerplate и ошибок.
В: Почему свежий проект на «OpenAPI 3» нельзя делать на swaggo/swag?
О: swaggo/swag генерирует Swagger 2.0, а не OpenAPI 3.x. Для 3.x нужен contract-first генератор (oapi-codegen, ogen) либо конвертация 2.0→3.0 с потерей нюансов. Плюс swaggo — code-first, спека вторична и дрейфует.
В: Как валидировать запросы против OpenAPI-спеки в рантайме в Go?
О: Через kin-openapi: загрузить и провалидировать спеку (openapi3.Loader, doc.Validate), построить роутер (gorillamux), в middleware найти маршрут (FindRoute) и вызвать openapi3filter.ValidateRequest. С oapi-codegen это упрощается через embedded-spec и OapiRequestValidator. Response-валидацию обычно держат в тестах/staging, не на проде.
В: Чем отличаются required и nullable и как это влияет на сгенерированный Go-код?
О: required — поле обязано присутствовать в объекте; nullable — значение может быть null. Это ортогонально. В Go: required и не nullable → значение по значению (T); иначе → указатель (*T) с omitempty. Путаница приводит к багам: отправка null против отсутствия ключа, неверная (де)сериализация.
В: Как в CI не дать сломать обратную совместимость API?
О: Линт спеки (spectral/vacuum) на качество и конвенции + diff против предыдущей версии (oasdiff breaking old new --fail-on ERR), который классифицирует изменения и блокирует merge при breaking. Breaking-изменения требуют бампа мажорной версии API и плана миграции (deprecated: true, Sunset/Deprecation заголовки).
В: Что считается breaking change в REST API? О: Удаление/переименование эндпоинта или поля ответа, добавление нового required-параметра/поля в запрос, ужесточение валидации входа, смена типа поля, сужение enum. Non-breaking: новый эндпоинт, новое optional-поле в ответе, новый optional-параметр, ослабление валидации.
В: Как подменить сгенерированный oapi-codegen тип на свой (например, decimal вместо float)?
О: Расширениями x-go-type и x-go-type-import прямо в схеме, либо через секцию маппинга типов в конфиге. Это позволяет отдать в код, скажем, decimal.Decimal вместо float64 для денежных сумм, сохранив контракт в спеке.
На что копают на senior+#
- Моделирование union/полиморфизма:
oneOf/anyOf/allOf+discriminator, как это ложится на Go (генерируемые union-типы,As/Mergeметоды), когда лучше денормализовать схему ради удобства кода. - Эволюция контракта: стратегия версионирования (path vs header vs media-type), deprecation policy,
Sunsetheader (RFC 8594), как катить breaking changes без даунтайма потребителей. - CI-гейтинг контракта: связка spectral/vacuum + oasdiff + проверка «generated code up to date» (перегенерить и
git diff --exit-code), запрет merge при дрейфе/breaking. - Где валидировать: статика vs рантайм vs только-тесты; стоимость response-валидации на проде; что делать при расхождении embedded-spec и задеплоенной спеки.
- Тулчейн-зрелость 3.0 vs 3.1: осознанный выбор версии под возможности генераторов/валидаторов команды, а не «берём новее».
- Организация спек в масштабе: монорепо vs отдельный
api-specsрепо, бандлинг$ref(redocly bundle), переиспользование общих схем (error-модель, pagination) между сервисами. - Контракт как граница команд: mock-сервера (Prism) для параллельной разработки, генерация клиентов фронта (
openapi-typescript/orval), consumer-driven contract testing (Pact) как дополнение к схеме. - Безопасность в спеке: корректное описание
securitySchemes(oauth2 flows, scopes), что спека описывает контракт, но не заменяет реальную проверку прав; не утечь в публичную доку внутренние эндпоинты. - Производительность валидации: стоимость рантайм-валидации тяжёлых тел, кэширование скомпилированных схем, выбор
ogen(без рефлексии) для горячих путей.