コンテンツにスキップ

Body - ネストされたモデル

FastAPI では、(Pydantic のおかげで)任意の深さにネストされたモデルを定義、検証、ドキュメント化、および使用できます。

リストフィールド

属性をサブタイプとして定義できます。たとえば、Python の list

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
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: Union[float, None] = None
    tags: list = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

これにより、tags はリストになりますが、リストの要素の型は宣言されません。

型パラメータ付きリストフィールド

ただし、Python には内部型、つまり「型パラメータ」を持つリストを宣言する特定の方法があります。

typing の List をインポートする

Python 3.9 以降では、以下で説明するように、標準の list を使用してこれらの型注釈を宣言できます。💡

ただし、3.9 より前の Python バージョン(3.6 以降)では、最初に標準 Python の typing モジュールから List をインポートする必要があります。

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: Union[float, None] = None
    tags: List[str] = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

型パラメータ付きの list を宣言する

listdicttuple のように、型パラメータ(内部型)を持つ型を宣言するには

  • Python 3.9 より前のバージョンの場合は、typing モジュールから対応するバージョンをインポートします。
  • 角かっこ:[ および ] を使用して、内部型を「型パラメータ」として渡します。

Python 3.9 では、次のようになります。

my_list: list[str]

3.9 より前のバージョンの Python では、次のようになります。

from typing import List

my_list: List[str]

これは、型宣言のための標準的な Python 構文です。

内部型を持つモデル属性には、同じ標準構文を使用します。

したがって、この例では、tags を特に「文字列のリスト」にすることができます。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
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: Union[float, None] = None
    tags: list[str] = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
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: Union[float, None] = None
    tags: List[str] = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

型を設定する

しかし、それを考えると、タグは繰り返されるべきではなく、おそらく一意の文字列になることに気づきます。

そして、Python には一意の項目のセットのための特別なデータ型 set があります。

次に、tags を文字列のセットとして宣言できます。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
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: Union[float, None] = None
    tags: set[str] = set()


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
from typing import Set, 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: Set[str] = set()


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

これにより、重複したデータを含むリクエストを受信した場合でも、一意の項目のセットに変換されます。

そして、そのデータを出力する際には、たとえソースに重複があったとしても、ユニークな項目の集合として出力されます。

そして、それに応じて注釈/ドキュメントも付与されます。

ネストされたモデル

Pydanticモデルの各属性には型があります。

しかし、その型自体が別のPydanticモデルであることもあります。

そのため、特定の属性名、型、およびバリデーションを持つ、深くネストされたJSON「オブジェクト」を宣言できます。

すべて、任意にネストさせることができます。

サブモデルの定義

例えば、Imageモデルを定義できます。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    image: Image | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


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


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
from typing import Set, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()
    image: Union[Image, None] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

サブモデルを型として使用する

そして、それを属性の型として使用できます。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    image: Image | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


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


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
from typing import Set, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()
    image: Union[Image, None] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

これは、FastAPIが次のようなボディを期待することを意味します。

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": ["rock", "metal", "bar"],
    "image": {
        "url": "http://example.com/baz.jpg",
        "name": "The Foo live"
    }
}

繰り返しになりますが、FastAPIを使用すると、宣言するだけで次のものが得られます。

  • エディターサポート(補完など)、ネストされたモデルの場合でも
  • データ変換
  • データ検証
  • 自動ドキュメント

特別な型とバリデーション

strintfloatなどの通常の単数型とは別に、strから継承するより複雑な単数型を使用できます。

利用可能なすべてのオプションについては、Pydanticの型概要を参照してください。次の章でいくつかの例を示します。

たとえば、Imageモデルではurlフィールドがあるので、strの代わりにPydanticのHttpUrlのインスタンスとして宣言できます。

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    image: Image | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


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


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
from typing import Set, Union

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()
    image: Union[Image, None] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

文字列が有効なURLであるかどうかチェックされ、JSON Schema/OpenAPIでドキュメント化されます。

サブモデルのリストを持つ属性

Pydanticモデルをlistsetなどのサブタイプとして使用することもできます。

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    images: list[Image] | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: set[str] = set()
    images: Union[list[Image], None] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
from typing import List, Set, Union

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


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


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

これにより、次のようなJSONボディを期待(変換、検証、ドキュメント化など)します。

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": [
        "rock",
        "metal",
        "bar"
    ],
    "images": [
        {
            "url": "http://example.com/baz.jpg",
            "name": "The Foo live"
        },
        {
            "url": "http://example.com/dave.jpg",
            "name": "The Baz"
        }
    ]
}

情報

imagesキーにイメージオブジェクトのリストが含まれていることに注目してください。

深くネストされたモデル

任意に深くネストされたモデルを定義できます。

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: set[str] = set()
    images: list[Image] | None = None


class Offer(BaseModel):
    name: str
    description: str | None = None
    price: float
    items: list[Item]


@app.post("/offers/")
async def create_offer(offer: Offer):
    return offer
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: set[str] = set()
    images: Union[list[Image], None] = None


class Offer(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    items: list[Item]


@app.post("/offers/")
async def create_offer(offer: Offer):
    return offer
from typing import List, Set, Union

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


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


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


@app.post("/offers/")
async def create_offer(offer: Offer):
    return offer

情報

OfferItemのリストを持ち、さらにItemがオプションでImageのリストを持つことに注目してください。

純粋なリストのボディ

期待するJSONボディのトップレベルの値がJSON array(Python list)の場合、Pydanticモデルと同じように、関数のパラメーターで型を宣言できます。

images: List[Image]

または、Python 3.9以降では

images: list[Image]

例えば、このように

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


@app.post("/images/multiple/")
async def create_multiple_images(images: list[Image]):
    return images
from typing import List

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


@app.post("/images/multiple/")
async def create_multiple_images(images: List[Image]):
    return images

あらゆる場所でのエディターサポート

そして、あらゆる場所でエディターサポートが得られます。

リスト内のアイテムについても同様です。

Pydanticモデルの代わりにdictを直接操作している場合、このようなエディターサポートは得られません。

しかし、それらを心配する必要もありません。受信したdictは自動的に変換され、出力も自動的にJSONに変換されます。

任意のdictのボディ

また、キーがある型で、値が別の型であるdictとしてボディを宣言することもできます。

この方法では、(Pydanticモデルの場合のように)有効なフィールド/属性名を事前に知る必要はありません。

これは、まだ知らないキーを受け取りたい場合に役立ちます。


もう1つの便利なケースは、(例:intのような)別の型のキーを使用したい場合です。

ここでそれを見ていきます。

この場合、intキーとfloatの値を持つ限り、任意のdictを受け入れます。

from fastapi import FastAPI

app = FastAPI()


@app.post("/index-weights/")
async def create_index_weights(weights: dict[int, float]):
    return weights
from typing import Dict

from fastapi import FastAPI

app = FastAPI()


@app.post("/index-weights/")
async def create_index_weights(weights: Dict[int, float]):
    return weights

ヒント

JSONはキーとしてstrのみをサポートしていることに注意してください。

しかし、Pydanticには自動データ変換があります。

つまり、APIクライアントはキーとして文字列のみを送信できますが、それらの文字列に純粋な整数が含まれている限り、Pydanticはそれらを変換して検証します。

そして、weightsとして受け取るdictには、実際にはintキーとfloatの値が含まれます。

まとめ

FastAPIを使用すると、コードをシンプル、短く、エレガントに保ちながら、Pydanticモデルによって提供される最大限の柔軟性を得ることができます。

しかし、すべての利点

  • エディターサポート(あらゆる場所で補完!)
  • データ変換(別名:解析/シリアライズ)
  • データ検証
  • スキーマドキュメント
  • 自動ドキュメント