Урок 07.02: Спецификация OpenAPI (Swagger) — углублённое руководство
Цель урока
Научиться описывать REST API с помощью спецификации OpenAPI 3.1 (бывший Swagger). Понять, как аналитик может использовать OpenAPI для документирования требований к API, написания Design-First контрактов, а также освоить полиморфизм схем данных — комбинирование моделей через allOf, anyOf и oneOf для описания сложных JSON-структур.
Ключевые понятия
| Термин | Определение |
|---|---|
| OpenAPI Specification (OAS) | Стандарт описания REST API в формате JSON или YAML (версия 3.1 — текущая) |
| Swagger | Экосистема инструментов вокруг OpenAPI (Swagger UI, Swagger Editor, Swagger Codegen) |
| YAML | Человеко-читаемый формат сериализации данных (как JSON, но с отступами) |
| Swagger UI | Интерактивная документация API (можно «потыкать» кнопками прямо в браузере) |
| Schema | Описание структуры данных (request/response body) |
| Component | Переиспользуемый блок: модель данных, параметр, ответ, security scheme |
$ref |
Ссылка на другой компонент (аналог #include или import) |
allOf |
Композиция: объединение нескольких схем в одну (наследование / расширение) |
oneOf |
Дискриминатор: ровно одна из перечисленных схем |
anyOf |
Объединение: одна или более из перечисленных схем |
| Design-First | Подход: сначала спецификация API, потом реализация |
| Code-First | Подход: сначала код, потом генерация спецификации |
1. Что такое OpenAPI и зачем он аналитику?
1.1. От Swagger к OpenAPI: история
| Год | Событие | Что изменилось |
|---|---|---|
| 2011 | Swagger создан компанией Reverb Technologies | Появился первый инструмент для описания REST API |
| 2015 | Спецификация передана Linux Foundation, переименована в OpenAPI | Стандарт стал открытым, не привязан к одной компании |
| 2017 | OpenAPI 3.0 — первая post-Swagger версия | oneOf, anyOf, example, переработанные responses |
| 2021 | OpenAPI 3.1 | Полная совместимость с JSON Schema 2020-12, webhooks, $defs |
Текущий статус: OpenAPI 3.1 — стандарт де-факто. Все новые API должны описываться на 3.1. Версия 3.0 — ещё в использовании, но миграция на 3.1 рекомендуется.
1.2. Зачем аналитику OpenAPI?
| Задача аналитика | Как помогает OpenAPI | Конкретика |
|---|---|---|
| Спецификация требований к API | Вместо «таблички в Confluence» — машиночитаемая спецификация на YAML | Разработчик не спрашивает «а что должно быть в ответе?» — он читает OpenAPI |
| Коммуникация с разработчиками | Разработчик сразу видит: эндпоинты, модели, статус-коды | Нет рассинхрона «аналитик думал одно, разработчик понял другое» |
| Коммуникация с заказчиком | Swagger UI — интерактивная документация | Заказчик нажимает «Try it out» и видит, как работает API |
| Генерация тестов | Из OpenAPI можно сгенерировать тест-кейсы | Postman, Rest-Assured, Karate — импорт из OpenAPI |
| Генерация клиентского кода | Из OpenAPI — SDK для JS, Python, Java, C# | Фронтенд не пишет fetch-запросы руками — генерирует SDK |
| Валидация запросов | API Gateway может проверять входящие запросы по OpenAPI | 422 возвращается автоматически, если JSON не соответствует схеме |
| Code Review API | OpenAPI-файл — артефакт, который ревьювят | Аналитик проверяет: все ли сценарии покрыты, нет ли дыр в контракте |
2. Структура OpenAPI-спецификации
2.1. Базовая структура (YAML)
Любая OpenAPI-спецификация состоит из нескольких корневых разделов:
openapi: "3.1.0" # Версия спецификации (обязательно)
info: # Метаданные (обязательно)
title: "Task Manager API"
description: "API для системы управления задачами"
version: "1.0.0"
contact:
name: "Команда разработки"
email: "dev@taskmanager.com"
license:
name: "MIT"
servers: # Базовые URL (опционально)
- url: https://api.taskmanager.com/api/v1
description: "Production"
- url: https://staging-api.taskmanager.com/api/v1
description: "Staging"
paths: # Эндпоинты — ядро спецификации
/tasks:
get:
summary: "Список задач"
# ... описание запроса, параметров, ответов
post:
summary: "Создать задачу"
# ...
components: # Переиспользуемые блоки
schemas:
Task:
type: object
properties:
id:
type: integer
title:
type: string
security: # Глобальная аутентификация
- BearerAuth: []
tags: # Группировка эндпоинтов (для UI)
- name: Tasks
description: "Управление задачами"
2.2. Обязательность разделов
| Раздел | Описание | Обязателен? | Типичная ошибка |
|---|---|---|---|
openapi |
Версия спецификации | ✅ Да | Не указана → парсер не знает, 3.0 или 3.1 |
info |
Метаданные API | ✅ Да | Нет version → нельзя трекать изменения |
servers |
Базовые URL | ❌ Нет | Если нет — Swagger UI показывает пустой URL |
paths |
Эндпоинты | ✅ Да (фактически) | Без paths спецификация бесполезна |
components |
Переиспользуемые схемы | ❌ Нет | Без components — дублирование схем в каждом эндпоинте |
security |
Аутентификация | ❌ Нет | Если нет — эндпоинты открыты всем |
tags |
Группировка | ❌ Нет | Без tags — Swagger UI показывает плоский список |
3. Описание эндпоинта (Path)
3.1. Структура эндпоинта
Каждый эндпоинт в paths содержит:
paths:
/tasks/{taskId}:
get: # HTTP-метод (get, post, put, patch, delete, ...)
operationId: getTaskById # ✅ Уникальный идентификатор (для генерации кода)
summary: "Получить задачу" # Краткое описание (одна строка)
description: | # Развёрнутое описание (можно с Markdown)
Возвращает задачу по её уникальному идентификатору.
Для неавторизованных пользователей возвращает 401.
Для несуществующих ID — 404.
tags: # Группировка в Swagger UI
- Tasks
parameters: # Параметры запроса
- name: taskId
in: path
required: true
schema:
type: integer
responses: # Ответы
"200":
description: "Задача найдена"
content:
application/json:
schema:
$ref: "#/components/schemas/Task"
"404":
$ref: "#/components/responses/NotFound"
security: # Специфичная аутентификация (переопределяет глобальную)
- BearerAuth: []
3.2. Параметры: in — где находится параметр
in |
Где находится | Пример OpenAPI | Пример URL |
|---|---|---|---|
path |
В пути URL | /tasks/{taskId} |
/tasks/42 |
query |
После ? |
?status=done&page=1 |
/tasks?status=done |
header |
В HTTP-заголовке | X-Request-ID |
(заголовок) |
cookie |
В cookie | session_id |
(cookie) |
Пример с path-параметром:
/tasks/{taskId}:
get:
parameters:
- name: taskId
in: path
required: true # path-параметры ОБЯЗАТЕЛЬНЫ всегда
description: "ID задачи"
schema:
type: integer
minimum: 1
Пример с query-параметрами:
/tasks:
get:
parameters:
- name: status
in: query
required: false
schema:
type: string
enum: [To Do, In Progress, Testing, Done, Blocked]
- name: page
in: query
schema:
type: integer
default: 1
minimum: 1
- name: limit
in: query
schema:
type: integer
default: 10
maximum: 100
3.3. Request Body (тело запроса) — для POST, PUT, PATCH
/tasks:
post:
requestBody:
required: true # Тело обязательно
content:
application/json: # Формат тела
schema:
$ref: "#/components/schemas/CreateTaskRequest"
application/xml: # Альтернативный формат
schema:
$ref: "#/components/schemas/CreateTaskRequest"
description: "Данные новой задачи"
3.4. Responses (ответы)
Для каждого статус-кода нужно описать:
responses:
"200": # Статус-код — строка! (в кавычках)
description: "Список задач"
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Task"
example: # Пример ответа (виден в Swagger UI)
- id: 1
title: "Настроить CI/CD"
status: "In Progress"
# ...
"401":
$ref: "#/components/responses/Unauthorized"
4. Компоненты (Components / Schemas)
4.1. Модели данных (Schemas)
components:
schemas:
# ===== БАЗОВЫЕ МОДЕЛИ =====
Task:
type: object
required: # Поля, которые ВСЕГДА есть в ответе
- id
- title
- status
- created_at
properties:
id:
type: integer
description: "Уникальный идентификатор задачи"
example: 42
title:
type: string
description: "Название задачи"
maxLength: 255
example: "Настроить CI/CD"
description:
type: string
nullable: true # Может быть null (явно указано)
description: "Описание задачи"
status:
type: string
enum: [To Do, In Progress, Testing, Done, Blocked]
description: "Текущий статус"
priority:
type: string
enum: [Low, Medium, High, Critical]
default: Medium
assignee:
allOf:
- $ref: "#/components/schemas/UserBrief"
nullable: true
created_at:
type: string
format: date-time # RFC 3339: 2026-05-29T10:30:00Z
deadline:
type: string
format: date # 2026-06-10
nullable: true
UserBrief:
type: object
description: "Краткая информация о пользователе (вложение в Task)"
properties:
id:
type: integer
example: 1
name:
type: string
example: "Анна"
email:
type: string
format: email
example: "anna@taskmanager.com"
# ===== ЗАПРОСЫ =====
CreateTaskRequest:
type: object
required:
- title
- project_id
properties:
title:
type: string
maxLength: 255
example: "Настроить CI/CD"
description:
type: string
priority:
type: string
enum: [Low, Medium, High, Critical]
default: Medium
assignee_id:
type: integer
nullable: true
project_id:
type: integer
deadline:
type: string
format: date
nullable: true
# ===== ПАГИНАЦИЯ =====
Pagination:
type: object
properties:
page:
type: integer
example: 1
limit:
type: integer
example: 10
total:
type: integer
example: 150
total_pages:
type: integer
example: 15
4.2. Ответы (Responses) — переиспользуемые ответы
components:
responses:
Unauthorized:
description: "Не авторизован — требуется JWT-токен"
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error:
code: "UNAUTHORIZED"
message: "Неверный или истёкший токен"
NotFound:
description: "Ресурс не найден"
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
ValidationError:
description: "Ошибка валидации данных"
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
schemas:
ErrorResponse:
type: object
description: "Стандартный формат ошибки (RFC 7807 Problem Details)"
properties:
error:
type: object
properties:
code:
type: string
example: "VALIDATION_ERROR"
message:
type: string
example: "Поле 'email' имеет неверный формат"
details:
type: array
items:
$ref: "#/components/schemas/ErrorDetail"
ErrorDetail:
type: object
properties:
field:
type: string
example: "email"
reason:
type: string
example: "not_an_email"
message:
type: string
example: "Поле должно содержать валидный email"
4.3. Аутентификация (Security)
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT # Опционально, для документации
description: "JWT-токен, полученный после /auth/login"
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: "API-ключ для сервис-сервисной интеграции"
Глобальная аутентификация (для всех эндпоинтов):
security:
- BearerAuth: []
Локальная (для конкретного эндпоинта — переопределяет глобальную):
/auth/login:
post:
security: [] # Нет аутентификации (публичный эндпоинт)
/tasks:
post:
security:
- BearerAuth: []
- ApiKeyAuth: [] # Можно и так, и так
5. 🧬 Полиморфизм схем: allOf, oneOf, anyOf
Одна из самых мощных возможностей OpenAPI 3.x — композиция схем. В реальных API редко бывает так, что все ответы имеют одинаковую структуру. Часто бывает:
- «Базовый ответ + расширенные поля для детального просмотра»
- «Платёж может быть картой, PayPal или криптой — у каждого своя структура»
- «Успешный ответ и ответ с ошибкой имеют разную форму»
Для этого существуют allOf, oneOf и anyOf.
5.1. allOf — объединение (AND / наследование)
allOf означает: «схема должна соответствовать ВСЕМ перечисленным схемам одновременно». Используется для:
- Наследования (базовая сущность + расширение)
- Переиспользования общих полей
- Композиции нескольких независимых блоков
Пример: базовая сущность Task → расширенное представление
Базовая схема Task (для списка задач):
components:
schemas:
Task:
description: "Базовая схема задачи (для списка)"
type: object
required:
- id
- title
- status
- created_at
properties:
id:
type: integer
title:
type: string
status:
type: string
enum: [To Do, In Progress, Testing, Done, Blocked]
priority:
type: string
enum: [Low, Medium, High, Critical]
assignee:
$ref: "#/components/schemas/UserBrief"
nullable: true
created_at:
type: string
format: date-time
deadline:
type: string
format: date
nullable: true
Расширенная схема TaskDetailed (для страницы задачи — наследует Task + добавляет поля):
components:
schemas:
TaskDetailed:
description: "Детальная информация о задаче (Task + комментарии + вложения + история)"
allOf:
- $ref: "#/components/schemas/Task" # Базовые поля
- type: object # Дополнительные поля
required:
- comments
- attachments
- history
properties:
comments:
type: array
description: "Список комментариев к задаче"
items:
$ref: "#/components/schemas/Comment"
attachments:
type: array
items:
$ref: "#/components/schemas/Attachment"
history:
type: array
description: "История изменений задачи"
items:
$ref: "#/components/schemas/TaskHistoryChange"
project:
$ref: "#/components/schemas/ProjectBrief"
description: "Краткая информация о проекте"
Как это работает:
TaskDetailed = Task (все поля) + { comments, attachments, history, project }
Использование в эндпоинтах:
paths:
/tasks:
get:
summary: "Список задач (базовые поля)"
responses:
"200":
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Task" # Базовый
/tasks/{taskId}:
get:
summary: "Детальная информация о задаче"
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/TaskDetailed" # Расширенный
Выгода: Базовая схема Task определена один раз. TaskDetailed расширяет её, не дублируя поля. Если в Task появится новое поле — оно автоматически будет и в TaskDetailed.
Пример 2: Стандартные поля ответа + данные (обёртка)
components:
schemas:
# Базовая обёртка для успешного ответа
ApiResponse:
type: object
properties:
success:
type: boolean
example: true
timestamp:
type: string
format: date-time
request_id:
type: string
format: uuid
# Ответ со списком задач (ApiResponse + data + pagination)
TaskListResponse:
allOf:
- $ref: "#/components/schemas/ApiResponse"
- type: object
required:
- data
- pagination
properties:
data:
type: array
items:
$ref: "#/components/schemas/Task"
pagination:
$ref: "#/components/schemas/Pagination"
5.2. oneOf — дискриминатор (XOR / один из)
oneOf означает: «схема должна соответствовать ровно ОДНОЙ из перечисленных схем». Используется для:
- Полиморфных ответов (платёж: карта / PayPal / крипта)
- Дискриминатора по полю (type = "card" → одна схема, type = "paypal" → другая)
- Успех vs Ошибка (но чаще используют общую обёртку)
Пример: разные типы платежей
components:
schemas:
CardPayment:
type: object
properties:
type:
type: string
enum: [card]
card_number:
type: string
pattern: '^\d{16}$'
cardholder_name:
type: string
expiry_date:
type: string
format: date
PayPalPayment:
type: object
properties:
type:
type: string
enum: [paypal]
paypal_email:
type: string
format: email
CryptoPayment:
type: object
properties:
type:
type: string
enum: [crypto]
wallet_address:
type: string
currency:
type: string
enum: [BTC, ETH, USDT]
# Полиморфная схема: платёж — это один из трёх типов
Payment:
oneOf:
- $ref: "#/components/schemas/CardPayment"
- $ref: "#/components/schemas/PayPalPayment"
- $ref: "#/components/schemas/CryptoPayment"
discriminator:
propertyName: type # Поле, по которому определяется тип
mapping:
card: "#/components/schemas/CardPayment"
paypal: "#/components/schemas/PayPalPayment"
crypto: "#/components/schemas/CryptoPayment"
Как это работает:
// Ответ API: платёж по карте
{
"type": "card",
"card_number": "4111111111111111",
"cardholder_name": "Иван Петров",
"expiry_date": "2028-12"
}
// Ответ API: платёж через PayPal
{
"type": "paypal",
"paypal_email": "ivan@example.com"
}
discriminator — ключевое слово, которое указывает: «смотри на поле type, по нему определи, какая из oneOf-схем используется». Без discriminator'а парсеру пришлось бы перебирать все схемы и проверять, какая подходит — это медленно и ненадёжно.
Пример: Webhook-события (типичный use case)
components:
schemas:
WebhookEvent:
oneOf:
- $ref: "#/components/schemas/TaskCreatedEvent"
- $ref: "#/components/schemas/TaskUpdatedEvent"
- $ref: "#/components/schemas/TaskDeletedEvent"
- $ref: "#/components/schemas/UserRegisteredEvent"
discriminator:
propertyName: event_type
mapping:
task.created: "#/components/schemas/TaskCreatedEvent"
task.updated: "#/components/schemas/TaskUpdatedEvent"
task.deleted: "#/components/schemas/TaskDeletedEvent"
user.registered: "#/components/schemas/UserRegisteredEvent"
TaskCreatedEvent:
type: object
properties:
event_type:
type: string
enum: [task.created]
task_id:
type: integer
title:
type: string
created_at:
type: string
format: date-time
TaskUpdatedEvent:
type: object
properties:
event_type:
type: string
enum: [task.updated]
task_id:
type: integer
changed_fields:
type: array
items:
type: string
new_status:
type: string
Эта схема
WebhookEventиспользуется как основа для описания исходящих webhook-уведомлений через ключевое словоwebhooks(OpenAPI 3.1) — подробнее в Уроке 07.07.
5.3. anyOf — нестрогое объединение (OR)
anyOf означает: «схема должна соответствовать ХОТЯ БЫ ОДНОЙ из перечисленных схем». Мягче, чем oneOf (может соответствовать нескольким).
Используется реже, но бывает полезно для:
- Частичной валидации (поле должно быть либо строкой, либо числом)
- Необязательных расширений
- Обратной совместимости (новое поле может быть в разных форматах)
Пример: поле, которое может быть строкой или объектом
components:
schemas:
FlexibleMetadata:
type: object
properties:
value:
anyOf:
- type: string
- type: number
- type: object
properties:
raw:
type: string
formatted:
type: string
description: "Мета-значение — может быть строкой, числом или объектом"
Пример: поисковый запрос (разные типы фильтров)
components:
schemas:
SearchFilter:
type: object
properties:
filters:
type: array
items:
anyOf:
- $ref: "#/components/schemas/TextFilter" # filter by text
- $ref: "#/components/schemas/DateFilter" # filter by date
- $ref: "#/components/schemas/NumberFilter" # filter by number
Важно: anyOf — самый «слабый» из композиций. Используйте его только когда реально нужно допустить несколько форматов. Если должно быть ровно одно — используйте oneOf. Если должны быть все — allOf.
5.4. Сравнительная таблица allOf vs oneOf vs anyOf
| Ключевое слово | Логика | JSON Schema | Аналогия в программировании | Когда использовать |
|---|---|---|---|---|
| allOf | AND — все схемы | {allOf: [A, B]} → A ∩ B |
extends, наследование, миксин | Базовая сущность + расширение |
| oneOf | XOR — ровно одна | {oneOf: [A, B]} → A xor B |
sum type, discriminated union, sealed class | Платёж (карта/PayPal/крипта), Webhook-события |
| anyOf | OR — хотя бы одна | {anyOf: [A, B]} → A ∪ B |
union type, mixed type | Поле может быть строкой ИЛИ числом |
5.5. Особенности OpenAPI 3.1: JSON Schema 2020-12
OpenAPI 3.1 — это надмножество JSON Schema 2020-12. Это значит:
- ✅ Можно использовать
$defsвместо (или вместе с)components/schemas - ✅ Поддерживаются все JSON Schema-ключевые слова:
if/then/else,patternProperties,dependentRequired - ✅ Можно ссылаться на внешние схемы:
$ref: "https://schemas.example.com/common/error.json" - ⚠️ НО:
exampleтеперь находится на уровне схемы, а не свойства.examples(множественное число) — тоже на уровне схемы.
Пример с JSON Schema-условием:
Payment:
type: object
properties:
type:
type: string
enum: [card, paypal, crypto]
card_number:
type: string
# Если type = "card", то card_number обязателен
if:
properties:
type:
const: card
then:
required:
- card_number
6. Инструменты OpenAPI
6.1. Swagger Editor
editor.swagger.io — онлайн-редактор. Пишете YAML слева → видите документацию справа. Есть подсветка синтаксиса и валидация ошибок.
6.2. Swagger UI
Интерактивная документация. Каждый эндпоинт — раскрывающийся блок с кнопкой «Try it out»:
GET /tasks ───────────────────────────────────────────
│ Parameters: status (query), page (query), limit (q) │
│ [Try it out] │
│ → status: "In Progress" │
│ → page: 1 │
│ → [Execute] │
│ Response: 200 │
│ [ { id: 1, title: "...", status: "In Progress" } ] │
└─────────────────────────────────────────────────────┘
6.3. Swagger Codegen (генерация кода)
Из OpenAPI-спецификации можно сгенерировать:
| Тип | Языки | Что получается |
|---|---|---|
| Клиентский SDK | JavaScript, TypeScript, Python, Java, C#, Go, PHP, Ruby, Dart | Готовые методы API: api.getTasks({status: 'done'}) |
| Серверный код | Spring Boot, Express.js, FastAPI, ASP.NET, Gin | Контроллеры с заглушками (stub) |
| Документация | HTML, Markdown, AsciiDoc | Статическая документация |
| API Gateway | AWS API Gateway, Kong, Tyk | Конфигурация прокси |
6.4. Postman / Insomnia
Postman импортирует OpenAPI-спецификацию и создаёт коллекцию запросов. Insomnia — аналогично.
6.5. Redoc
Альтернатива Swagger UI — redocly.com/redoc. Генерирует более красивую (но не интерактивную) документацию.
7. Design-First: проектирование контракта до кода
7.1. Что такое Design-First?
Design-First (или Contract-First) — это подход к разработке API, при котором сначала пишется OpenAPI-спецификация, а уже потом — код сервера и клиента.
Design-First:
Требования → OpenAPI (YAML) → Согласование → Сервер + Клиент параллельно
Code-First:
Требования → Код сервера → OpenAPI (генерация) → Клиент
7.2. Почему Design-First для аналитика — стандарт?
| Критерий | Design-First | Code-First |
|---|---|---|
| Аналитик контролирует контракт | ✅ Да | ❌ Нет (диктует разработчик) |
| Заказчик видит API до начала разработки | ✅ Через Swagger UI | ❌ Только когда готов бэкенд |
| Документация не расходится с реализацией | ✅ OpenAPI — источник правды | ⚠️ Может отличаться |
| Параллельная разработка (фронтенд + бэкенд) | ✅ Да (по контракту) | ❌ Фронтенд ждёт бэкенд |
| Генерация тестов | ✅ Раньше, до кода | ❌ Позже, когда есть код |
7.3. Правила хорошего тона при проектировании OpenAPI-контракта
Правило 1: operationId — уникальный и понятный
# ✅ Хорошо
operationId: getTasks
operationId: getTaskById
operationId: createTask
operationId: updateTaskStatus
operationId: deleteTask
# ❌ Плохо
operationId: get
operationId: get1
operationId: doSomething
Зачем: operationId используется для генерации имён методов в SDK. api.getTasks() — читаемо, api.get() — нет.
Правило 2: nullable явно указывайте
# ✅ Хорошо
description:
type: string
nullable: true # Явно: может быть null
# ❌ Плохо
description:
type: string # Непонятно: null возможен? Или это обязательное поле?
Правило 3: readOnly и writeOnly для полей, которые различаются в запросе и ответе
Task:
type: object
properties:
id:
type: integer
readOnly: true # id есть в ответе, НО НЕ в запросе
title:
type: string
created_at:
type: string
format: date-time
readOnly: true # Сервер генерирует дату
assignee_id:
type: integer
writeOnly: true # Есть в запросе, НО НЕ в ответе (в ответе — assignee объект)
Правило 4: Всегда используйте enum для статусов и фиксированных списков
# ✅ Хорошо: enum
status:
type: string
enum: [To Do, In Progress, Testing, Done, Blocked]
description: "Статус задачи"
# ❌ Плохо: без enum
status:
type: string
description: "Статус задачи"
# Разработчик может написать "todo", "TODO", "To do" — консистентности нет
Правило 5: Ответ с ошибкой — единый формат
Стандартизируйте формат ошибок (RFC 7807 Problem Details или свой):
# Единый формат для всех ошибок
ErrorResponse:
type: object
properties:
error:
type: object
properties:
code:
type: string # MACHINE_READABLE, а не человеческий текст
message:
type: string # Человекочитаемое описание
details:
type: array
items:
$ref: "#/components/schemas/ErrorDetail"
Никогда не делайте для каждой ошибки свой формат: {"error": "не найдено"} в одном месте и {"message": "ошибка"} в другом.
Правило 6: version в info — не версия API, а версия спецификации
info:
version: "1.2.0" # Версия OpenAPI-спецификации, растёт с изменениями
# а НЕ версия продукта (она может быть в теге)
При каждом изменении OpenAPI-файла увеличивайте версию. Это позволяет клиентам понять: «Ага, я работал с v1.1.0, а текущая — v1.2.0, нужно проверить изменения».
Правило 7: Один endpoint — один operationId (даже для разных методов)
/tasks/{taskId}:
get:
operationId: getTaskById # УНИКАЛЬНО
patch:
operationId: updateTask # УНИКАЛЬНО
delete:
operationId: deleteTask # УНИКАЛЬНО
Правило 8: Пагинация — как отдельный компонент
# В components/schemas/Pagination — единожды
# В каждом списковом ответе — $ref
responses:
"200":
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Task"
pagination:
$ref: "#/components/schemas/Pagination"
Правило 9: Минимальные и максимальные значения — указывайте
page:
type: integer
minimum: 1
default: 1
title:
type: string
minLength: 1 # Не может быть пустым
maxLength: 255
Правило 10: Всегда описывайте все ответы (не только успешные)
responses:
"200": ...
"400": $ref: ... # Плохой запрос
"401": $ref: ... # Не авторизован
"404": $ref: ... # Не найдено
"422": $ref: ... # Валидация
"500": $ref: ... # Внутренняя ошибка
Если вы не опишете 500 — разработчик не будет обрабатывать эту ошибку на клиенте, и пользователь увидит белый экран.
7.4. Чек-лист проверки OpenAPI-спецификации (Code Review)
| № | Проверка | ✅ / ❌ |
|---|---|---|
| 1 | Все operationId уникальны и понятны |
|
| 2 | У каждого эндпоинта описаны минимум 200, 400, 401, 500 | |
| 3 | У каждой схемы есть description и example |
|
| 4 | nullable явно указан для полей, которые могут быть null |
|
| 5 | readOnly / writeOnly для полей, которые различаются в request/response |
|
| 6 | Формат ошибок единый (везде $ref на одну схему) |
|
| 7 | Enum используются для статусов и фиксированных списков | |
| 8 | Пагинация — компонент (не дублируется в каждом эндпоинте) | |
| 9 | Версия спецификации в info.version соответствует изменениям |
|
| 10 | security указана (глобально или локально) |
|
| 11 | Нет секретов (паролей, токенов) в example | |
| 12 | Типы данных корректны: integer для id, date-time для дат, email для email |
7.5. Процесс работы аналитика с OpenAPI
1. СБОР ТРЕБОВАНИЙ
│
├── Use Case / User Story
├── BPMN-диаграмма процесса
└── Список сущностей (из ERD)
│
▼
2. ПРОЕКТИРОВАНИЕ КОНТРАКТА (аналитик)
│
├── Определить ресурсы (/tasks, /users, /projects)
├── Определить методы для каждого ресурса
├── Определить модели данных (Task, User, ...)
└── Написать OpenAPI YAML (в Swagger Editor)
│
▼
3. РЕВЬЮ КОНТРАКТА (аналитик + разработчик + архитектор)
│
├── Проверить: все ли сценарии покрыты?
├── Проверить: все ли ошибки описаны?
├── Согласовать: формат ответа, пагинацию, версионирование
└── Утвердить OpenAPI-спецификацию
│
▼
4. ПУБЛИКАЦИЯ + ГЕНЕРАЦИЯ
│
├── Swagger UI → заказчик видит и тестирует
├── Swagger Codegen → клиентский SDK (фронтенд)
├── Swagger Codegen → серверный stub (бэкенд)
└── Автоматические тесты (каждый PR проверяет, что API соответствует OpenAPI)
│
▼
5. РАЗРАБОТКА И ТЕСТИРОВАНИЕ
│
├── Фронтенд использует SDK (не пишет fetch-запросы руками)
├── Бэкенд реализует контроллеры по контракту
└── Тесты проверяют: ответы соответствуют OpenAPI
8. Распространённые ошибки в OpenAPI
| Ошибка | ❌ Неправильно | ✅ Правильно | Последствия |
|---|---|---|---|
| Статус-код в кавычках/без | 200: (число) |
"200": (строка) |
YAML может сконвертировать число |
| Нет nullable | type: string (может быть null) |
type: string, nullable: true |
Клиент упадёт, если пришёл null |
| Enum не на все статусы | enum: [Done, Blocked] (неполный) |
enum: [To Do, In Progress, ...] |
Клиент не знает о существовании других статусов |
| Ответ 500 не описан | Только 200, 404 | 200, 400, 401, 404, 422, 500 | Клиент не обрабатывает 500 |
| Дублирование схем | Одна и та же модель повторена в 3 местах | $ref на components/schemas |
Изменение нужно вносить в 3 местах |
| Формат ошибки разный | {error: "..."} в одном месте, {message: "..."} в другом |
Единая схема ErrorResponse | Клиент не может парсить ошибки |
| Нет example | Только типы | Есть example для каждой модели | Swagger UI показывает «string», а не реальный пример |
Вопросы для самопроверки
Базовый уровень
- Что такое OpenAPI? Чем он отличается от экосистемы Swagger?
- Какие три раздела обязательно должны быть в OpenAPI-спецификации?
- Как описать path-параметр (
/tasks/{id}) и query-параметр (?status=done)? - Что такое
$refи как он помогает избежать дублирования? - Как в OpenAPI описать модель данных: типы, обязательные поля, enum, nullable?
Продвинутый уровень (полиморфизм схем)
- allOf: В чём разница между
allOf: [Task, ExtendedFields]и простым перечислением всех полей в одной схеме? КогдаallOfпредпочтительнее? - oneOf: Спроектируйте схему для ответа API, который может вернуть либо объект
User, либо объектError. Какой ключ использовать —oneOfилиanyOf? Нужен лиdiscriminator? - anyOf: Приведите пример, когда
anyOf— единственный правильный выбор (oneOf не подходит). - Design-First: В проекте фронтенд и бэкенд разрабатываются параллельно. Какой подход (Design-First или Code-First) обеспечит параллельную разработку? Почему?
- Чек-лист: Перечислите 5 проверок, которые аналитик должен сделать при Code Review OpenAPI-спецификации.
Практическое задание
Задание 1. Напишите OpenAPI-спецификацию (2 балла)
Напишите OpenAPI 3.1 спецификацию (в YAML) для следующего API системы управления задачами:
Эндпоинты:
| Метод | URL | Описание |
|---|---|---|
| GET | /tasks | Список задач (фильтры: status, priority, assignee_id; пагинация: page, limit) |
| POST | /tasks | Создать задачу |
| GET | /tasks/{taskId} | Получить задачу по ID |
| PATCH | /tasks/{taskId}/status | Изменить статус задачи |
| DELETE | /tasks/{taskId} | Удалить задачу |
| GET | /users | Список пользователей |
| POST | /auth/login | Авторизация (email + password → токен) |
Модели данных (обязательные):
- Task: id, title, description (nullable), status (enum: To Do, In Progress, Testing, Done, Blocked), priority (enum с default), assignee (UserBrief, nullable), project_id, created_at, deadline (nullable)
- UserBrief: id, name, email
- CreateTaskRequest: title (required), description, priority (default), assignee_id (nullable), project_id (required), deadline (nullable)
- AuthRequest: email (required), password (required)
- AuthResponse: token (string), user (UserBrief)
- ErrorResponse: единый формат с code, message, details
- Pagination: page, limit, total, total_pages
Требования:
- Используйте
$refдля переиспользования компонентов - Все эндпоинты, кроме
/auth/login, требуют Bearer-аутентификацию - Укажите ответы: 200, 201, 204, 400, 401, 404, 422 (где релевантно)
- Добавьте
operationIdдля каждого эндпоинта - Используйте
nullableдля опциональных полей - Добавьте
exampleдля каждой модели
Задание 2. Полиморфизм схем: allOf + oneOf (1,5 балла)
Ситуация: Вы проектируете API для системы уведомлений. В системе есть три типа уведомлений:
| Тип | Поля |
|---|---|
| TaskAssignedNotification (назначена задача) | id, type="task_assigned", user_id, task_id, task_title, assigned_by, created_at |
| StatusChangedNotification (изменён статус) | id, type="status_changed", user_id, task_id, old_status, new_status, created_at |
| MentionNotification (упоминание в комментарии) | id, type="mention", user_id, comment_id, comment_author, comment_preview, created_at |
Требуется:
2.1. (0,5 балла) Создайте базовую схему BaseNotification с общими полями (id, type, user_id, created_at). Используйте allOf для создания конкретных схем уведомлений, расширяющих базовую.
2.2. (0,5 балла) Создайте полиморфную схему Notification с oneOf + discriminator по полю type, которая объединяет все три типа уведомлений.
2.3. (0,5 балла) Опишите эндпоинт GET /notifications (список уведомлений), который возвращает массив Notification (полиморфный). Укажите ответ 200 и 401.
Формат ответа: YAML-фрагмент OpenAPI 3.1 с компонентами schemas и paths.
Задание 3. Design-First: ревью контракта (0,5 балла)
Дан фрагмент OpenAPI-спецификации:
openapi: "3.1.0"
info:
title: "Task Manager"
version: "1.0"
paths:
/tasks:
get:
summary: "Get tasks"
responses:
"200":
description: "OK"
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: string
status:
type: string
created:
type: string
post:
summary: "Create"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
title:
type: string
project_id:
type: integer
responses:
"200":
description: "OK"
/tasks/{id}:
delete:
summary: "Delete"
responses:
"200":
description: "OK"
Найдите и исправьте минимум 8 ошибок/несоответствий. Напишите исправленный фрагмент.
Подсказки к ошибкам:
- Версия info.version без patch (не по semver)
- Нет nullable, нет enum, нет example
- Нет operationId
- Нет аутентификации
- Нет описания ошибок (4xx, 5xx)
- POST возвращает 200 вместо 201
- DELETE возвращает 200 вместо 204
- Поле
nameвместоtitleв Task - Поле
createdвместоcreated_atс format: date-time - model Task не вынесена в components (дублирование в других эндпоинтах)
Задание 4. Анализ OpenAPI-спецификации (1 балл)
Дана OpenAPI-спецификация (фрагмент) для PATCH-эндпоинта:
/tasks/{taskId}:
patch:
operationId: updateTask
summary: "Частичное обновление задачи"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateTaskRequest"
responses:
"200":
description: "Задача обновлена"
content:
application/json:
schema:
$ref: "#/components/schemas/Task"
"422":
$ref: "#/components/responses/ValidationError"
Вопросы:
- (0,3 балла) Какие три статус-кода ещё нужно описать для этого эндпоинта? Почему?
- (0,3 балла) Как должна выглядеть схема
UpdateTaskRequest, чтобы подчеркнуть, что это частичное обновление (PATCH), а не полная замена? - (0,4 балла) Напишите схему
UpdateTaskRequest, в которой:- Ни одно поле не отмечено как
required(все опциональны — PATCH) - Если поле не передано — сервер его не трогает
- Поля: title (string, max 255), description (string, nullable), priority (enum), status (enum), assignee_id (integer, nullable), deadline (string, date, nullable)
- Ни одно поле не отмечено как
Дополнительные материалы
- Спецификация: OpenAPI 3.1 — spec.openapis.org/oas/v3.1.0
- Спецификация: JSON Schema 2020-12 — json-schema.org/specification
- Инструмент: Swagger Editor — editor.swagger.io
- Инструмент: Swagger UI — генерация интерактивной документации
- Инструмент: Redoc — альтернатива Swagger UI, красивая статическая документация
- Инструмент: Postman — импорт OpenAPI, создание коллекций
- Инструмент: OpenAPI Generator — openapi-generator.tech (альтернатива Swagger Codegen)
- Книга: «Designing APIs with Swagger and OpenAPI» — практическое руководство
- Книга: «API Design Patterns» — JJ Geewax, главы по полиморфизму OpenAPI
- Статья: «OpenAPI 3.1 — What's New» — swagger.io/blog
- Практика: Конвертировать существующий API (Petstore) из JSON в YAML и обратно
- Практика: Petstore OpenAPI — petstore.swagger.io — живой пример для изучения