Дискриминированные объединения в Swagger/OpenAPI
Полное руководство по дискриминированным объединениям в OpenAPI. Создавайте условные схемы для TypeScript union типов в Swagger UI с примерами кода.
Как определить условную схему в Swagger/OpenAPI на основе TypeScript union‑типа?
Здравствуйте! У меня есть два типа тел запросов в 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?
- Описание запроса POST с дискриминированным объединением
- Описание ответа GET с условными схемами
- Пример в Swagger UI
- Расширенные возможности и лучшие практики
- Ограничения и альтернативные подходы
- Заключение
- Источники
Что такое дискриминированные объединения в OpenAPI?
Дискриминированные объединения в OpenAPI позволяют описать несколько схем, выбор между которыми определяется значением определённого поля (дискриминатора). Это механизм, аналогичный TypeScript union‑типам, где TypeScript использует поле type для определения, какой именно объект в объединении используется.
Ключевые компоненты:
- Дискриминатор — поле, по которому определяется схема
- Выбор схемы — разные значения дискриминатора приводят к разным схемам
- Валидация — OpenAPI гарантирует, что объект соответствует одной из схем
discriminator:
propertyName: type
mapping:
action1: '#/components/schemas/Action1Payload'
action2: '#/components/schemas/Action2Payload'
Описание запроса POST с дискриминированным объединением
Для вашего TypeScript‑типа TBody можно создать следующую OpenAPI‑спецификацию:
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
Использование в пути запроса:
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 с условными схемами
Для ответов с дискриминированными объединениями используется тот же подход:
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
Пример конечной точки:
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:
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. Вложенные дискриминированные объединения
Для сложных структур можно создавать вложенные дискриминаторы:
NestedPayload:
oneOf:
- $ref: '#/components/schemas/NestedAction1'
- $ref: '#/components/schemas/NestedAction2'
discriminator:
propertyName: nestedType
mapping:
nested1: '#/components/schemas/NestedAction1'
nested2: '#/components/schemas/NestedAction2'
3. Пример с enum и дополнительными ограничениями
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 без дискриминатора
TBody:
anyOf:
- $ref: '#/components/schemas/Action1Body'
- $ref: '#/components/schemas/Action2Body'
Недостаток: нет автоматической проверки на основе поля type.
Вариант 2: Разные эндпоинты для разных типов
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:
@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> {
// ...
}
}
Заключение
-
Дискриминированные объединения являются мощным механизмом для описания условных схем в OpenAPI, аналогично TypeScript union‑типам.
-
Основные компоненты: дискриминатор (
discriminator), схемы вариантов (oneOf) и маппинг (mapping) позволяют создавать гибкие и точные спецификации. -
Практическое применение: как для запросов (requestBody), так и для ответов (responses) можно использовать один и тот же подход с дискриминаторами.
-
Swagger UI предоставляет интерактивный интерфейс, где пользователи могут выбирать тип объекта и видеть соответствующие формы для заполнения.
-
Рекомендации:
- Используйте OpenAPI 3.0+ для полной поддержки дискриминаторов
- Добавляйте подробные описания и валидацию для каждого варианта схемы
- Рассмотрите автоматизацию с помощью инструментов вроде tsoa для упрощения поддержки
-
Альтернативы: при сложных сценариях можно использовать разделение на разные эндпоинты или комбинацию с
anyOfдля большей гибкости.
Этот подход позволяет создавать API с точной валидацией и улучшенным пользовательским опытом в Swagger UI, полностью соответствуя TypeScript union‑типам.