Другое

Дискриминированные объединения в Swagger/OpenAPI

Полное руководство по дискриминированным объединениям в OpenAPI. Создавайте условные схемы для TypeScript union типов в Swagger UI с примерами кода.

Как определить условную схему в Swagger/OpenAPI на основе TypeScript union‑типа?

Здравствуйте! У меня есть два типа тел запросов в TypeScript:

typescript
type TBody = {
  type: 'action1'
  payload: {
    value: number
    action: string
  }
} | {
  type: 'action2'
  payload: null
}

Возможно ли описать такое тело запроса, например, для POST‑запроса в Swagger/OpenAPI, чтобы на основе JSON‑схемы генерировать запросы? Чтобы в Swagger UI можно было отправлять данные в зависимости от значения поля type.

Тот же вопрос касается и ответов на запросы, например, для GET. Можно ли описать данные, которые возвращаются в зависимости от какого-либо поля?

Если это возможно, было бы здорово увидеть пример того, как это выглядит в Swagger UI.

В OpenAPI можно описать условные схемы на основе TypeScript union‑типов с помощью механизма дискриминированных объединений (discriminated unions). Для вашего примера с TBody это возможно реализовать, используя поле type как дискриминатор и указав различные схемы для каждого возможного значения этого поля.


Содержание


Что такое дискриминированные объединения в OpenAPI?

Дискриминированные объединения в OpenAPI позволяют описать несколько схем, выбор между которыми определяется значением определённого поля (дискриминатора). Это механизм, аналогичный TypeScript union‑типам, где TypeScript использует поле type для определения, какой именно объект в объединении используется.

Ключевые компоненты:

  • Дискриминатор — поле, по которому определяется схема
  • Выбор схемы — разные значения дискриминатора приводят к разным схемам
  • Валидация — OpenAPI гарантирует, что объект соответствует одной из схем
yaml
discriminator:
  propertyName: type
  mapping:
    action1: '#/components/schemas/Action1Payload'
    action2: '#/components/schemas/Action2Payload'

Описание запроса POST с дискриминированным объединением

Для вашего TypeScript‑типа TBody можно создать следующую OpenAPI‑спецификацию:

yaml
components:
  schemas:
    # Базовая схема с дискриминатором
    TBody:
      oneOf:
        - $ref: '#/components/schemas/Action1Body'
        - $ref: '#/components/schemas/Action2Body'
      discriminator:
        propertyName: type
        mapping:
          action1: '#/components/schemas/Action1Body'
          action2: '#/components/schemas/Action2Body'
    
    # Схема для action1
    Action1Body:
      type: object
      required:
        - type
        - payload
      properties:
        type:
          type: string
          enum: [action1]
        payload:
          type: object
          required:
            - value
            - action
          properties:
            value:
              type: number
            action:
              type: string
    
    # Схема для action2
    Action2Body:
      type: object
      required:
        - type
        - payload
      properties:
        type:
          type: string
          enum: [action2]
        payload:
          type: object
          nullable: true
          properties: {}
          additionalProperties: false

Использование в пути запроса:

yaml
paths:
  /api/actions:
    post:
      summary: Выполнение действия
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TBody'
      responses:
        '200':
          description: Успешное выполнение
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TResponse'

Описание ответа GET с условными схемами

Для ответов с дискриминированными объединениями используется тот же подход:

yaml
components:
  schemas:
    TResponse:
      oneOf:
        - $ref: '#/components/schemas/SuccessResponse'
        - $ref: '#/components/schemas/ErrorResponse'
      discriminator:
        propertyName: status
        mapping:
          success: '#/components/schemas/SuccessResponse'
          error: '#/components/schemas/ErrorResponse'
    
    SuccessResponse:
      type: object
      required:
        - status
        - data
      properties:
        status:
          type: string
          enum: [success]
        data:
          type: object
          properties:
            result:
              type: string
            timestamp:
              type: string
              format: date-time
    
    ErrorResponse:
      type: object
      required:
        - status
        - error
      properties:
        status:
          type: string
          enum: [error]
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string

Пример конечной точки:

yaml
paths:
  /api/status:
    get:
      summary: Получение статуса
      responses:
        '200':
          description: Ответ с зависимостью от статуса
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TResponse'

Пример в Swagger UI

В Swagger UI описанные выше схемы будут отображаться следующим образом:

Для POST‑запроса:

  • Swagger UI покажет выпадающий список для выбора типа: action1 или action2
  • При выборе action1 появятся поля:
    • type (с возможным значением только action1)
    • payload.value (число)
    • payload.action (строка)
  • При выборе action2 появятся поля:
    • type (с возможным значением только action2)
    • payload (null)

Для GET‑ответа:

  • Swagger UI покажет примеры ответов для каждого возможного статуса
  • Будут представлены оба варианта ответа с соответствующими полями

Визуально это выглядит как интерактивный интерфейс, где пользователь выбирает тип объекта, и автоматически появляется форма с соответствующими полями.


Расширенные возможности и лучшие практики

1. Использование allOf для общих полей

Если у разных схем есть общие поля, можно использовать allOf:

yaml
BaseResponse:
  type: object
  properties:
    requestId:
      type: string
      format: uuid
    timestamp:
      type: string
      format: date-time

SuccessResponse:
  allOf:
    - $ref: '#/components/schemas/BaseResponse'
    - type: object
      properties:
        status:
          type: string
          enum: [success]
        data:
          # ... специфичные поля для успешного ответа

2. Вложенные дискриминированные объединения

Для сложных структур можно создавать вложенные дискриминаторы:

yaml
NestedPayload:
  oneOf:
    - $ref: '#/components/schemas/NestedAction1'
    - $ref: '#/components/schemas/NestedAction2'
  discriminator:
    propertyName: nestedType
    mapping:
      nested1: '#/components/schemas/NestedAction1'
      nested2: '#/components/schemas/NestedAction2'

3. Пример с enum и дополнительными ограничениями

yaml
Action1Body:
  type: object
  required:
    - type
    - payload
  properties:
    type:
      type: string
      enum: [action1]
      description: Тип действия
    payload:
      type: object
      required:
        - value
        - action
      properties:
        value:
          type: number
          minimum: 0
          maximum: 100
        action:
          type: string
          pattern: "^[a-zA-Z0-9_]+$"

Ограничения и альтернативные подходы

1. Ограничения OpenAPI

  • Версия 3.0: Дискриминаторы поддерживаются только в OpenAPI 3.0+
  • Ограниченная поддержка: Не все инструменты генерации кода полностью поддерживают дискриминаторы
  • Сложность отладки: Ошибки в определении дискриминаторов могут быть трудноотличимы

2. Альтернативные подходы

Вариант 1: Использование anyOf без дискриминатора

yaml
TBody:
  anyOf:
    - $ref: '#/components/schemas/Action1Body'
    - $ref: '#/components/schemas/Action2Body'

Недостаток: нет автоматической проверки на основе поля type.

Вариант 2: Разные эндпоинты для разных типов

yaml
paths:
  /api/actions/action1:
    post:
      requestBody:
        $ref: '#/components/schemas/Action1Body'
  
  /api/actions/action2:
    post:
      requestBody:
        $ref: '#/components/schemas/Action2Body'

Преимущество: более простая валидация, недостаток: несколько эндпоинтов.

3. Инструменты и библиотеки

Для автоматической генерации OpenAPI‑спецификации из TypeScript можно использовать:

  • tsoa: Автоматическая генерация OpenAPI из TypeScript декораторов
  • typescript-openapi: Генерация спецификации на основе типов
  • swagger‑typescript‑api: Утилита для генерации API‑клиентов

Пример использования tsoa:

typescript
@Route('api/actions')
@TsoaResponse<Action1Body>(200, 'Action 1')
@TsoaResponse<Action2Body>(200, 'Action 2')
export class ActionsController {
  @Post()
  public async createAction(@Body() body: TBody): Promise<TResponse> {
    // ...
  }
}

Заключение

  1. Дискриминированные объединения являются мощным механизмом для описания условных схем в OpenAPI, аналогично TypeScript union‑типам.

  2. Основные компоненты: дискриминатор (discriminator), схемы вариантов (oneOf) и маппинг (mapping) позволяют создавать гибкие и точные спецификации.

  3. Практическое применение: как для запросов (requestBody), так и для ответов (responses) можно использовать один и тот же подход с дискриминаторами.

  4. Swagger UI предоставляет интерактивный интерфейс, где пользователи могут выбирать тип объекта и видеть соответствующие формы для заполнения.

  5. Рекомендации:

    • Используйте OpenAPI 3.0+ для полной поддержки дискриминаторов
    • Добавляйте подробные описания и валидацию для каждого варианта схемы
    • Рассмотрите автоматизацию с помощью инструментов вроде tsoa для упрощения поддержки
  6. Альтернативы: при сложных сценариях можно использовать разделение на разные эндпоинты или комбинацию с anyOf для большей гибкости.

Этот подход позволяет создавать API с точной валидацией и улучшенным пользовательским опытом в Swagger UI, полностью соответствуя TypeScript union‑типам.


Источники

  1. OpenAPI Specification 3.0 - Discriminator Object
  2. Swagger UI Documentation - Interactive Features
  3. OpenAPI Best Practices - Using oneOf and anyOf
  4. tsoa - TypeScript OpenAPI router
  5. Microsoft OpenAPI Guidelines - Discriminated Unions
Авторы
Проверено модерацией
Модерация