コンテンツへスキップ

入出力でOpenAPIスキーマを分離するかどうか

Pydantic v2を使用すると、生成されるOpenAPIは以前よりも少し正確で正しくなります。 😎

実際、場合によっては、デフォルト値があるかどうかによって、入力と出力用に、同じPydanticモデルに対してOpenAPIに2つのJSONスキーマが作成されることさえあります。

それがどのように機能するか、および必要に応じてそれを変更する方法を見てみましょう。

入力と出力のためのPydanticモデル

デフォルト値を持つPydanticモデルがあるとします。例えば、このようなものです。

from fastapi import FastAPI
from pydantic import BaseModel


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

# Code below omitted 👇
👀 ファイルの完全なプレビュー
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"),
    ]
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel


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

# Code below omitted 👇
👀 ファイルの完全なプレビュー
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

# Code below omitted 👇
👀 ファイルの完全なプレビュー
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 👇
👀 ファイルの完全なプレビュー
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"),
    ]
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

# Code below omitted 👇
👀 ファイルの完全なプレビュー
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

# Code below omitted 👇
👀 ファイルの完全なプレビュー
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"),
    ]
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フィールドの1つに何も追加していない場合でも、JSONレスポンスにはデフォルト値(null)が含まれています。

これは、常に値を持つことを意味します。ただし、値がNone(またはJSONではnull)の場合があるだけです。

つまり、APIを使用するクライアントは、値が存在するかどうかを確認する必要はなく、フィールドは常に存在すると想定できますが、場合によってはNoneのデフォルト値を持つだけです。

これをOpenAPIで記述する方法は、そのフィールドが常に存在するため、requiredとマークすることです。

そのため、モデルのJSONスキーマは、入力用か出力用かによって異なる場合があります。

  • 入力の場合、description必須ではありません
  • 出力の場合、description必須です(場合によってはNone、JSON用語ではnull)。

ドキュメントにおける出力用モデル

ドキュメントの出力モデルを確認すると、namedescription両方赤いアスタリスク必須とマークされていることがわかります。

ドキュメントにおける入力および出力用モデル

OpenAPIで利用可能なすべてのスキーマ(JSONスキーマ)を確認すると、Item-InputItem-Outputの2つがあることがわかります。

Item-Inputの場合、description必須ではありません。赤いアスタリスクはありません。

しかし、Item-Outputの場合、description必須であり、赤いアスタリスクが付いています。

Pydantic v2のこの機能により、APIドキュメントがより正確になり、自動生成されたクライアントとSDKがある場合、それらもより正確になり、より優れた開発者体験と一貫性が得られます。🎉

スキーマを分離しない

さて、入力と出力に同じスキーマを使用したい場合もあります。

おそらく、これの主なユースケースは、すでに自動生成されたクライアントコード/SDKがあり、自動生成されたクライアントコード/SDKをすべてまだ更新したくない場合です。おそらくいつかはそうするでしょうが、今すぐではないかもしれません。

その場合は、FastAPIseparate_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"),
    ]
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と同じ動作です。🤓