Перейти к содержанию

Разделять схемы OpenAPI для входа и выхода или нет

При использовании Pydantic v2 сгенерированный OpenAPI становится чуть более точным и корректным, чем раньше. 😎

На самом деле, в некоторых случаях в OpenAPI будет даже две JSON схемы для одной и той же Pydantic‑модели: для входа и для выхода — в зависимости от наличия значений по умолчанию.

Посмотрим, как это работает, и как это изменить при необходимости.

Pydantic‑модели для входа и выхода

Предположим, у вас есть Pydantic‑модель со значениями по умолчанию, как здесь:

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None

# Code below omitted 👇
👀 Full file preview
from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
🤓 Other versions and variants
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> List[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]

Модель для входа

Если использовать эту модель как входную, как здесь:

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item

# Code below omitted 👇
👀 Full file preview
from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
🤓 Other versions and variants
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> List[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]

…то поле description не будет обязательным, потому что у него значение по умолчанию None.

Входная модель в документации

В документации это видно: у поля description нет красной звёздочки — оно не отмечено как обязательное:

Модель для выхода

Но если использовать ту же модель как выходную, как здесь:

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
🤓 Other versions and variants
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None


app = FastAPI()


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> List[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]

…то, поскольку у description есть значение по умолчанию, даже если вы ничего не вернёте для этого поля, оно всё равно будет иметь это значение по умолчанию.

Модель для данных ответа

Если поработать с интерактивной документацией и посмотреть ответ, то, хотя код ничего не добавил в одно из полей description, JSON‑ответ содержит значение по умолчанию (null):

Это означает, что у него всегда будет какое‑то значение, просто иногда это значение может быть None (или null в JSON).

Следовательно, клиентам, использующим ваш API, не нужно проверять наличие этого значения: они могут исходить из того, что поле всегда присутствует, а в некоторых случаях имеет значение по умолчанию None.

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

Из‑за этого JSON Schema для модели может отличаться в зависимости от использования для входа или выхода:

  • для входа description не будет обязательным
  • для выхода оно будет обязательным (и при этом может быть None, или, в терминах JSON, null)

Выходная модель в документации

В документации это тоже видно, что оба: name и description, помечены красной звёздочкой как обязательные:

Модели для входа и выхода в документации

Если посмотреть все доступные схемы (JSON Schema) в OpenAPI, вы увидите две: Item-Input и Item-Output.

Для Item-Input поле description не является обязательным — красной звёздочки нет.

А для Item-Output description обязательно — красная звёздочка есть.

Благодаря этой возможности Pydantic v2 документация вашего API становится более точной; если у вас есть сгенерированные клиенты и SDK, они тоже будут точнее, с лучшим удобством для разработчиков и большей консистентностью. 🎉

Не разделять схемы

Однако бывают случаи, когда вы хотите иметь одну и ту же схему для входа и выхода.

Главный сценарий — когда у вас уже есть сгенерированный клиентский код/SDK, и вы пока не хотите обновлять весь этот автогенерируемый код/SDK (рано или поздно вы это сделаете, но не сейчас).

В таком случае вы можете отключить эту функциональность в FastAPI с помощью параметра separate_input_output_schemas=False.

Информация

Поддержка separate_input_output_schemas появилась в FastAPI 0.102.0. 🤓

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: str | None = None


app = FastAPI(separate_input_output_schemas=False)


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
🤓 Other versions and variants
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None


app = FastAPI(separate_input_output_schemas=False)


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> list[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]
from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Union[str, None] = None


app = FastAPI(separate_input_output_schemas=False)


@app.post("/items/")
def create_item(item: Item):
    return item


@app.get("/items/")
def read_items() -> List[Item]:
    return [
        Item(
            name="Portal Gun",
            description="Device to travel through the multi-rick-verse",
        ),
        Item(name="Plumbus"),
    ]

Одна и та же схема для входной и выходной моделей в документации

Теперь для этой модели будет одна общая схема и для входа, и для выхода — только Item, и в ней description будет не обязательным:

Это то же поведение, что и в Pydantic v1. 🤓