コンテンツにスキップ

レスポンスモデル

path operations のいずれにおいても、response_modelパラメータを使用して、レスポンスのモデルを宣言することができます:

  • @app.get()
  • @app.post()
  • @app.put()
  • @app.delete()
  • など。
from typing import Any, List, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: List[str] = []


@app.post("/items/", response_model=Item)
async def create_item(item: Item) -> Any:
    return item


@app.get("/items/", response_model=List[Item])
async def read_items() -> Any:
    return [
        {"name": "Portal Gun", "price": 42.0},
        {"name": "Plumbus", "price": 32.0},
    ]

備考

response_modelは「デコレータ」メソッド(getpostなど)のパラメータであることに注意してください。すべてのパラメータやボディのように、path operation関数 のパラメータではありません。

Pydanticモデルの属性に対して宣言するのと同じ型を受け取るので、Pydanticモデルになることもできますが、例えば、List[Item]のようなPydanticモデルのlistになることもできます。

FastAPIはresponse_modelを使って以下のことをします:

  • 出力データを型宣言に変換します。
  • データを検証します。
  • OpenAPIの path operation で、レスポンス用のJSON Schemaを追加します。
  • 自動ドキュメントシステムで使用されます。

しかし、最も重要なのは:

  • 出力データをモデルのデータに限定します。これがどのように重要なのか以下で見ていきましょう。

技術詳細

レスポンスモデルは、関数の戻り値のアノテーションではなく、このパラメータで宣言されています。なぜなら、パス関数は実際にはそのレスポンスモデルを返すのではなく、dictやデータベースオブジェクト、あるいは他のモデルを返し、response_modelを使用してフィールドの制限やシリアライズを行うからです。

同じ入力データの返却

ここではUserInモデルを宣言しています。それには平文のパスワードが含まれています:

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Union[str, None] = None


# Don't do this in production!
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
    return user

そして、このモデルを使用して入力を宣言し、同じモデルを使って出力を宣言しています:

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Union[str, None] = None


# Don't do this in production!
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
    return user

これで、ブラウザがパスワードを使ってユーザーを作成する際に、APIがレスポンスで同じパスワードを返すようになりました。

この場合、ユーザー自身がパスワードを送信しているので問題ないかもしれません。

しかし、同じモデルを別のpath operationに使用すると、すべてのクライアントにユーザーのパスワードを送信してしまうことになります。

危険

ユーザーの平文のパスワードを保存したり、レスポンスで送信したりすることは絶対にしないでください。

出力モデルの追加

代わりに、平文のパスワードを持つ入力モデルと、パスワードを持たない出力モデルを作成することができます:

from typing import Any, Union

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Union[str, None] = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Union[str, None] = None


@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
    return user

ここでは、path operation関数がパスワードを含む同じ入力ユーザーを返しているにもかかわらず:

from typing import Any, Union

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Union[str, None] = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Union[str, None] = None


@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
    return user

...response_modelUserOutと宣言したことで、パスワードが含まれていません:

from typing import Any, Union

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Union[str, None] = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Union[str, None] = None


@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
    return user

そのため、FastAPI は出力モデルで宣言されていない全てのデータをフィルタリングしてくれます(Pydanticを使用)。

ドキュメントを見る

自動ドキュメントを見ると、入力モデルと出力モデルがそれぞれ独自のJSON Schemaを持っていることが確認できます。

そして、両方のモデルは、対話型のAPIドキュメントに使用されます:

レスポンスモデルのエンコーディングパラメータ

レスポンスモデルにはデフォルト値を設定することができます:

from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: float = 10.5
    tags: List[str] = []


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
    return items[item_id]
  • description: str = NoneNoneがデフォルト値です。
  • tax: float = 10.510.5がデフォルト値です。
  • tags: List[str] = [] は空のリスト([])がデフォルト値です。

しかし、実際に保存されていない場合には結果からそれらを省略した方が良いかもしれません。

例えば、NoSQLデータベースに多くのオプション属性を持つモデルがあるが、デフォルト値でいっぱいの非常に長いJSONレスポンスを送信したくない場合です。

response_model_exclude_unsetパラメータの使用

path operation デコレータresponse_model_exclude_unset=Trueパラメータを設定することができます:

from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: float = 10.5
    tags: List[str] = []


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
    return items[item_id]

そして、これらのデフォルト値はレスポンスに含まれず、実際に設定された値のみが含まれます。

そのため、path operationにIDfooが設定されたitemのリクエストを送ると、レスポンスは以下のようになります(デフォルト値を含まない):

{
    "name": "Foo",
    "price": 50.2
}

情報

FastAPIはこれをするために、Pydanticモデルの.dict()そのexclude_unsetパラメータで使用しています。

情報

以下も使用することができます:

  • response_model_exclude_defaults=True
  • response_model_exclude_none=True

exclude_defaultsexclude_noneについては、Pydanticのドキュメントで説明されている通りです。

デフォルト値を持つフィールドの値を持つデータ

しかし、IDbarのitemのように、デフォルト値が設定されているモデルのフィールドに値が設定されている場合:

{
    "name": "Bar",
    "description": "The bartenders",
    "price": 62,
    "tax": 20.2
}

それらはレスポンスに含まれます。

デフォルト値と同じ値を持つデータ

IDbazのitemのようにデフォルト値と同じ値を持つデータの場合:

{
    "name": "Baz",
    "description": None,
    "price": 50.2,
    "tax": 10.5,
    "tags": []
}

FastAPIは十分に賢いので(実際には、Pydanticが十分に賢い)descriptiontaxtagsはデフォルト値と同じ値を持っているにもかかわらず、明示的に設定されていることを理解しています。(デフォルトから取得するのではなく)

そのため、それらはJSONレスポンスに含まれることになります。

豆知識

デフォルト値はNoneだけでなく、なんでも良いことに注意してください。 例えば、リスト([])や10.5floatなどです。

response_model_includeresponse_model_exclude

path operationデコレータとしてresponse_model_includeresponse_model_excludeも使用することができます。

属性名を持つstrsetを受け取り、含める(残りを省略する)か、除外(残りを含む)します。

これは、Pydanticモデルが1つしかなく、出力からいくつかのデータを削除したい場合のクイックショートカットとして使用することができます。

豆知識

それでも、これらのパラメータではなく、複数のクラスを使用して、上記のようなアイデアを使うことをおすすめします。

これはresponse_model_includeresponse_mode_excludeを使用していくつかの属性を省略しても、アプリケーションのOpenAPI(とドキュメント)で生成されたJSON Schemaが完全なモデルになるからです。

同様に動作するresponse_model_by_aliasにも当てはまります。

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: float = 10.5


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
    "baz": {
        "name": "Baz",
        "description": "There goes my baz",
        "price": 50.2,
        "tax": 10.5,
    },
}


@app.get(
    "/items/{item_id}/name",
    response_model=Item,
    response_model_include={"name", "description"},
)
async def read_item_name(item_id: str):
    return items[item_id]


@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"})
async def read_item_public_data(item_id: str):
    return items[item_id]

豆知識

{"name", "description"}の構文はこれら2つの値をもつsetを作成します。

これはset(["name", "description"])と同等です。

setの代わりにlistを使用する

もしsetを使用することを忘れて、代わりにlisttupleを使用しても、FastAPIはそれをsetに変換して正しく動作します:

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: float = 10.5


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
    "baz": {
        "name": "Baz",
        "description": "There goes my baz",
        "price": 50.2,
        "tax": 10.5,
    },
}


@app.get(
    "/items/{item_id}/name",
    response_model=Item,
    response_model_include=["name", "description"],
)
async def read_item_name(item_id: str):
    return items[item_id]


@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude=["tax"])
async def read_item_public_data(item_id: str):
    return items[item_id]

まとめ

path operationデコレータのresponse_modelパラメータを使用して、レスポンスモデルを定義し、特にプライベートデータがフィルタリングされていることを保証します。

明示的に設定された値のみを返すには、response_model_exclude_unsetを使用します。