Что такое полиморфизм и зачем он нужен в Open API?
Давайте разбираться.
Полиморфизм — это сложное слово, но идея простая. Представьте, что у вас есть разные объекты, которые похожи друг на друга, но имеют некоторые отличия. Например, у вас есть домашние животные: кошки, собаки, птицы. У всех есть имя и возраст, но у кошек есть длина усов, у собак — размер лап, у птиц — размах крыльев.
В Swagger (OpenAPI) полиморфизм помогает описать такие объекты в вашем API так, чтобы:
- Не дублировать описание общих свойств.
- Упростить добавление новых типов объектов.
- Сделать документацию более понятной.
Раньше, когда нужно было описать объекты с общими и уникальными свойствами, приходилось копировать и вставлять общие части в каждое описание. Это было неудобно и приводило к ошибкам.
Чтобы решить эту проблему, в Swagger добавили специальные инструменты:
- oneOf
- anyOf
- allOf
- discriminator
Они позволяют описывать объекты более гибко и эффективно, избегая дублирования и делая API более понятным.
Рассмотрим каждый из четырёх инструментов подробнее!
Пример 1: oneOf — объект может быть одним из нескольких типов
Ситуация: У нас есть API для добавления домашнего животного. Животное может быть либо кошкой, либо собакой.
{
"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":"Длина хвоста"
}
}
}
}
}
}
Как это работает:
- oneOf говорит, что Pet может быть либо Cat, либо Dog.
- Общие свойства (name, age) описаны один раз в Pet.
- Уникальные свойства (whiskersLength для кошки, tailLength для собаки) описаны в своих схемах.
Пример запроса для добавления кошки:
{
"name": "Мурка",
"age": 3,
"whiskersLength": 7.5
}
Пример запроса для добавления собаки:
{
"name": "Бобик",
"age": 5,
"tailLength": 15.0
}
Пример 2: anyOf — объект может соответствовать любой из нескольких схем
Ситуация: У нас есть API для обновления профиля пользователя. Пользователь может обновить электронную почту, номер телефона или оба сразу.
{
"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": "Номер телефона"
}
}
}
}
}
}
Как это работает:
- 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": "Особая характеристика"
}
}
}
]
}
}
}
}
Как это работает:
- allOf объединяет свойства BaseProduct и дополнительные свойства SpecialProduct.
- SpecialProduct будет иметь id, name и specialFeature.
Пример объекта SpecialProduct:
{
"id": "prod123",
"name": "Уникальный продукт",
"specialFeature": "Лимитированная серия"
}
Пример 4: Использование discriminator для определения типа объекта
Ситуация: У нас есть API для обработки платежей. Платеж может быть через кредитную карту или PayPal.
{
"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"
}
}
}
}
}
}
Как это работает:
- 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.
А что, если разобрать еще более интересный пример, готовы?
Погнали!
Представьте, что вы - владелец пиццерии, и у вас есть меню, где клиенты могут создавать свои собственные пиццы. Но пиццы бывают разные:
- Базовая пицца: только тесто и соус.
- Пицца с добавками: базовая пицца плюс различные ингредиенты.
- Комбо-пицца: сочетание нескольких пицц в одной.
Вы хотите создать 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): это простая пицца с выбором теста. Клиент выбирает размер и тип теста.
2. Пицца с добавками (ToppingPizza): это базовая пицца плюс дополнительные ингредиенты, такие как сыр, пепперони, грибы и т.д.
3. Комбо-пицца (ComboPizza): это комбинация из нескольких пицц в одной. Например, половина пиццы — с сыром и пепперони, другая половина — с грибами и оливками.
- 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 облегчает поддержку и расширение функциональности.
- Масштабируемость: легко добавить новые виды пицц или опции, не нарушая существующую структуру.
*** Официальную документацию можете посмотреть здесь:
Подписывайтесь на наш ТГ-канал и ждем вас в комментариях под последним постом в нашем комьюнити: