Спецификация OpenAPI (Swagger) — углублённое руководство

Урок 2 из 8

Урок 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», а не реальный пример

Вопросы для самопроверки

Базовый уровень

  1. Что такое OpenAPI? Чем он отличается от экосистемы Swagger?
  2. Какие три раздела обязательно должны быть в OpenAPI-спецификации?
  3. Как описать path-параметр (/tasks/{id}) и query-параметр (?status=done)?
  4. Что такое $ref и как он помогает избежать дублирования?
  5. Как в OpenAPI описать модель данных: типы, обязательные поля, enum, nullable?

Продвинутый уровень (полиморфизм схем)

  1. allOf: В чём разница между allOf: [Task, ExtendedFields] и простым перечислением всех полей в одной схеме? Когда allOf предпочтительнее?
  2. oneOf: Спроектируйте схему для ответа API, который может вернуть либо объект User, либо объект Error. Какой ключ использовать — oneOf или anyOf? Нужен ли discriminator?
  3. anyOf: Приведите пример, когда anyOf — единственный правильный выбор (oneOf не подходит).
  4. Design-First: В проекте фронтенд и бэкенд разрабатываются параллельно. Какой подход (Design-First или Code-First) обеспечит параллельную разработку? Почему?
  5. Чек-лист: Перечислите 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

Требования:

  1. Используйте $ref для переиспользования компонентов
  2. Все эндпоинты, кроме /auth/login, требуют Bearer-аутентификацию
  3. Укажите ответы: 200, 201, 204, 400, 401, 404, 422 (где релевантно)
  4. Добавьте operationId для каждого эндпоинта
  5. Используйте nullable для опциональных полей
  6. Добавьте 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"

Вопросы:

  1. (0,3 балла) Какие три статус-кода ещё нужно описать для этого эндпоинта? Почему?
  2. (0,3 балла) Как должна выглядеть схема UpdateTaskRequest, чтобы подчеркнуть, что это частичное обновление (PATCH), а не полная замена?
  3. (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 — живой пример для изучения

📚 Материалы модуля

🖼️ Схема и инфографика

🎬 Видео-лекция

🎬 API и интеграции

📄 Дополнительные материалы (PDF)

📄API Integration Blueprints
Скачать
Спросить ИИ