Найти в Дзене

Полиморфизм в Open API (Swagger): что за зверь и с чем его едят?

Оглавление

Что такое полиморфизм и зачем он нужен в Open API?

Давайте разбираться.

Полиморфизм — это сложное слово, но идея простая. Представьте, что у вас есть разные объекты, которые похожи друг на друга, но имеют некоторые отличия. Например, у вас есть домашние животные: кошки, собаки, птицы. У всех есть имя и возраст, но у кошек есть длина усов, у собак — размер лап, у птиц — размах крыльев.

В Swagger (OpenAPI) полиморфизм помогает описать такие объекты в вашем API так, чтобы:

  • Не дублировать описание общих свойств.
  • Упростить добавление новых типов объектов.
  • Сделать документацию более понятной.

Раньше, когда нужно было описать объекты с общими и уникальными свойствами, приходилось копировать и вставлять общие части в каждое описание. Это было неудобно и приводило к ошибкам.

Чтобы решить эту проблему, в Swagger добавили специальные инструменты:

  • oneOf
  • anyOf
  • allOf
  • discriminator

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

Рассмотрим каждый из четырёх инструментов подробнее!

Пример 1: oneOf — объект может быть одним из нескольких типов

Ситуация: У нас есть API для добавления домашнего животного. Животное может быть либо кошкой, либо собакой.

-2
{
"components":{
"schemas":{
"Pet":{
"type":"object",
"required":[
"name",
"age"
],
"properties":{
"name":{
"type":"string"
},
"age":{
"type":"integer"
}
},
"oneOf":[
{
"$ref":"#/components/schemas/Cat"
},
{
"$ref":"#/components/schemas/Dog"
}
]
},
"Cat":{
"type":"object",
"properties":{
"whiskersLength":{
"type":"number",
"description":"Длина усов"
}
}
},
"Dog":{
"type":"object",
"properties":{
"tailLength":{
"type":"number",
"description":"Длина хвоста"
}
}
}
}
}
}

-3

Как это работает:

  • oneOf говорит, что Pet может быть либо Cat, либо Dog.
  • Общие свойства (name, age) описаны один раз в Pet.
  • Уникальные свойства (whiskersLength для кошки, tailLength для собаки) описаны в своих схемах.

Пример запроса для добавления кошки:

{
"name": "Мурка",
"age": 3,
"whiskersLength": 7.5
}

Пример запроса для добавления собаки:

{
"name": "Бобик",
"age": 5,
"tailLength": 15.0
}

Пример 2: anyOf — объект может соответствовать любой из нескольких схем

Ситуация: У нас есть API для обновления профиля пользователя. Пользователь может обновить электронную почту, номер телефона или оба сразу.

-4
{
"components": {
"schemas": {
"UserUpdate": {
"anyOf": [
{
"$ref": "#/components/schemas/EmailUpdate"
},
{
"$ref": "#/components/schemas/PhoneUpdate"
}
]
},
"EmailUpdate": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email"
}
}
},
"PhoneUpdate": {
"type": "object",
"properties": {
"phoneNumber": {
"type": "string",
"description": "Номер телефона"
}
}
}
}
}
}
-5

Как это работает:

  • anyOf позволяет объекту соответствовать любой из указанных схем или даже нескольким одновременно.
  • Пользователь может обновить только email, только телефон или оба сразу.

Примеры запросов:

1. Обновление только email:

{
"email": "new_email@example.com"
}

2. Обновление только номера телефона:

{
"phoneNumber": "+1234567890"
}

3. Обновление email и номера телефона:

{
"email": "new_email@example.com",
"phoneNumber": "+1234567890"
}

Пример 3: allOf — объединение нескольких схем

Ситуация: У нас есть базовый продукт, и нам нужно создать специальный продукт, который наследует свойства базового и добавляет новые.

Как вы думаете, что это за продукт?)
Как вы думаете, что это за продукт?)
{
"components": {
"schemas": {
"BaseProduct": {
"type": "object",
"required": ["id", "name"],
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"SpecialProduct": {
"allOf": [
{
"$ref": "#/components/schemas/BaseProduct"
},
{
"type": "object",
"properties": {
"specialFeature": {
"type": "string",
"description": "Особая характеристика"
}
}
}
]
}
}
}
}
-7

Как это работает:

  • allOf объединяет свойства BaseProduct и дополнительные свойства SpecialProduct.
  • SpecialProduct будет иметь id, name и specialFeature.

Пример объекта SpecialProduct:

{
"id": "prod123",
"name": "Уникальный продукт",
"specialFeature": "Лимитированная серия"
}

Пример 4: Использование discriminator для определения типа объекта

Ситуация: У нас есть API для обработки платежей. Платеж может быть через кредитную карту или PayPal.

-8
{
"components": {
"schemas": {
"Payment": {
"type": "object",
"required": ["paymentType"],
"properties": {
"paymentType": {
"type": "string"
}
},
"discriminator": {
"propertyName": "paymentType"
},
"oneOf": [
{
"$ref": "#/components/schemas/CreditCardPayment"
},
{
"$ref": "#/components/schemas/PayPalPayment"
}
]
},
"CreditCardPayment": {
"type": "object",
"properties": {
"paymentType": {
"type": "string",
"enum": ["CreditCard"]
},
"cardNumber": {
"type": "string"
},
"cardHolderName": {
"type": "string"
}
}
},
"PayPalPayment": {
"type": "object",
"properties": {
"paymentType": {
"type": "string",
"enum": ["PayPal"]
},
"email": {
"type": "string"
}
}
}
}
}
}
-9

Как это работает:

  • discriminator использует свойство paymentType для определения, какая схема должна применяться.
  • Если paymentType равен CreditCard, применяется схема CreditCardPayment.
  • Если paymentType равен PayPal, применяется схема PayPalPayment.

Пример запроса для платежа кредитной картой:

{
"paymentType": "CreditCard",
"cardNumber": "4111111111111111",
"cardHolderName": "Иван Иванов"
}

Пример запроса для платежа через PayPal:

{
"paymentType": "PayPal",
"email": "user@example.com"
}

РЕЗЮМИРУЕМ

Вы сейчас освоили крутые инструменты для работы с API! Закрепим теорию:

  • oneOf — объект должен подходить под ровно одну из схем. Это как выбрать одно блюдо из меню!
  • anyOf — объект может подходить под любую схему, или даже под несколько сразу. Тут можно смешивать вкусы!
  • allOf — объект объединяет свойства всех схем, чтобы создать что-то цельное, как комбо-блюдо!
  • discriminator — помогает выбрать правильную схему по значению свойства, как шеф-повар, решающий, какой рецепт применить. 🍲
Теперь у вас в руках суперспособности для создания гибких и мощных API.

А что, если разобрать еще более интересный пример, готовы?

Погнали!

Представьте, что вы - владелец пиццерии, и у вас есть меню, где клиенты могут создавать свои собственные пиццы. Но пиццы бывают разные:

-10
  • Базовая пицца: только тесто и соус.
  • Пицца с добавками: базовая пицца плюс различные ингредиенты.
  • Комбо-пицца: сочетание нескольких пицц в одной.

Вы хотите создать API, чтобы клиенты могли заказывать любые пиццы с разными опциями, не запутываясь в сложностях. Здесь нам помогут инструменты полиморфизма в Swagger:

  • allOf: чтобы объединять общие свойства пиццы с уникальными для каждого типа.
  • oneOf: чтобы выбрать конкретный тип пиццы.
  • anyOf: чтобы позволить добавлять любые дополнительные ингредиенты.
  • discriminator: чтобы определить, какой именно тип пиццы заказан.

Полная схема API

{
"openapi": "3.0.3",
"info": {
"title": "Pizza Order API",
"version": "1.0.0"
},
"paths": {
"/orders": {
"post": {
"summary": "Заказать пиццу",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"oneOf": [
{ "$ref": "#/components/schemas/BasicPizza" },
{ "$ref": "#/components/schemas/ToppingPizza" },
{ "$ref": "#/components/schemas/ComboPizza" }
],
"discriminator": {
"propertyName": "pizzaType"
}
}
}
}
},
"responses": {
"201": {
"description": "Заказ принят"
}
}
}
}
},
"components": {
"schemas": {
"Pizza": {
"type": "object",
"required": ["pizzaType", "size"],
"properties": {
"pizzaType": {
"type": "string",
"description": "Тип пиццы"
},
"size": {
"type": "string",
"enum": ["Small", "Medium", "Large"],
"description": "Размер пиццы"
}
},
"discriminator": {
"propertyName": "pizzaType"
}
},
"BasicPizza": {
"allOf": [
{ "$ref": "#/components/schemas/Pizza" },
{
"type": "object",
"properties": {
"pizzaType": {
"type": "string",
"enum": ["Basic"]
},
"crustType": {
"type": "string",
"enum": ["Thin", "Thick"],
"description": "Тип теста"
}
}
}
]
},
"ToppingPizza": {
"allOf": [
{ "$ref": "#/components/schemas/Pizza" },
{
"type": "object",
"required": ["toppings"],
"properties": {
"pizzaType": {
"type": "string",
"enum": ["Topping"]
},
"crustType": {
"type": "string",
"enum": ["Thin", "Thick"],
"description": "Тип теста"
},
"toppings": {
"type": "array",
"items": {
"type": "string",
"enum": ["Cheese", "Pepperoni", "Mushrooms", "Olives", "Onions"]
},
"description": "Дополнительные ингредиенты"
}
}
}
]
},
"ComboPizza": {
"allOf": [
{ "$ref": "#/components/schemas/Pizza" },
{
"type": "object",
"required": ["combinations"],
"properties": {
"pizzaType": {
"type": "string",
"enum": ["Combo"]
},
"combinations": {
"type": "array",
"items": {
"oneOf": [
{ "$ref": "#/components/schemas/BasicPizza" },
{ "$ref": "#/components/schemas/ToppingPizza" }
],
"discriminator": {
"propertyName": "pizzaType"
}
},
"description": "Сочетание пицц"
}
}
}
]
}
}
}
}

Подробнее изучим схему

1. Базовая пицца (BasicPizza): это простая пицца с выбором теста. Клиент выбирает размер и тип теста.

-11

2. Пицца с добавками (ToppingPizza): это базовая пицца плюс дополнительные ингредиенты, такие как сыр, пепперони, грибы и т.д.

-12

3. Комбо-пицца (ComboPizza): это комбинация из нескольких пицц в одной. Например, половина пиццы — с сыром и пепперони, другая половина — с грибами и оливками.

-13
  • Pizza: базовая схема, содержащая общие свойства всех пицц, такие как pizzaType и size.
  • allOf: используется, чтобы каждая конкретная пицца наследовала свойства от Pizza и добавляла свои уникальные свойства.
  • oneOf в requestBody: определяет, что заказанная пицца должна соответствовать одному из типов пицц.
  • anyOf: неявно используется в массиве toppings, позволяя клиенту выбирать любые дополнительные ингредиенты.
  • discriminator: свойство pizzaType помогает определить, какую именно схему использовать при обработке заказа.

Как это будет выглядеть в запросах REST API?

1. Заказ базовой пиццы

Запрос:

{
"pizzaType": "Basic",
"size": "Medium",
"crustType": "Thin"
}
  • Клиент заказал среднюю базовую пиццу с тонким тестом.

2. Заказ пиццы с добавками

Запрос:

{
"pizzaType": "Topping",
"size": "Large",
"crustType": "Thick",
"toppings": ["Cheese", "Pepperoni", "Olives"]
}
  • Клиент заказал большую пиццу с толстым тестом и добавками: сыр, пепперони и оливки.

3. Заказ комбо-пиццы

Запрос:

{
"pizzaType": "Combo",
"size": "Large",
"combinations": [
{
"pizzaType": "Topping",
"size": "Large",
"crustType": "Thin",
"toppings": ["Mushrooms", "Onions"]
},
{
"pizzaType": "Basic",
"size": "Large",
"crustType": "Thick"
}
]
}
  • Клиент заказал большую комбо-пиццу, состоящую из:
  • Первой половины: большая пицца с тонким тестом и добавками — грибы и лук.
  • Второй половины: большая базовая пицца с толстым тестом.

Как это работает вместе?

  • Наследование свойств с помощью allOf: каждая конкретная пицца наследует общие свойства из Pizza и добавляет свои уникальные свойства.
  • Выбор схемы с помощью oneOf: в корне схемы заказа мы используем oneOf, чтобы указать, что заказ должен соответствовать одной из схем пиццы (BasicPizza, ToppingPizza или ComboPizza).
  • Комбинирование пицц с помощью oneOf и discriminator: в ComboPizza мы используем массив combinations, где каждая пицца определяется с помощью oneOf и discriminator, позволяя включать разные типы пицц в одну комбо-пиццу.
  • Неявное использование anyOf: в свойстве toppings клиента может выбрать любые дополнительные ингредиенты из предложенных, что фактически является использованием anyOf.
  • Определение типа пиццы с помощью discriminator: свойство pizzaType помогает API определить, какую схему использовать для обработки заказа.
Этот простой и вкусный пример показывает, как можно использовать инструменты полиморфизма в Swagger для описания API пиццерии
  • allOf: для наследования общих свойств и добавления специфических для каждого типа пицц
  • oneOf: для выбора конкретного типа пиццы в заказе
  • anyOf: позволяет клиенту выбирать любые комбинации дополнительных ингредиентов
  • discriminator: помогает однозначно определить тип пиццы и применить правильную схему

Ещё раз отметим, почему классно использовать эти подходы:

  • Гибкость: клиенты могут заказывать различные комбинации пицц по своему вкусу.
  • Удобство для разработчиков: ясная и структурированная схема API облегчает поддержку и расширение функциональности.
  • Масштабируемость: легко добавить новые виды пицц или опции, не нарушая существующую структуру.

*** Официальную документацию можете посмотреть здесь:

OpenAPI Specification v3.1.0

Подписывайтесь на наш ТГ-канал и ждем вас в комментариях под последним постом в нашем комьюнити:

API. Архитектура. Веб-сервисы