コンテンツへスキップ

入力と出力で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


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 👇
👀 ファイル全体のプレビュー
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フィールドは必須ではありません。デフォルト値が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でこれを記述する方法は、そのフィールドが常に存在するため、必須としてマークすることです。

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

  • 入力用の場合、description必須ではありません
  • 出力用の場合、必須となります(そして、おそらくNone、つまりJSON用語ではnullになります)。

ドキュメントでの出力モデル

ドキュメントでも出力モデルを確認できます。namedescription両方赤いアスタリスク必須とマークされています。

ドキュメントでの入力と出力用モデル

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

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

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

このPydantic v2の機能により、APIドキュメントはより正確になり、自動生成されたクライアントや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と同じ動作です。🤓