コンテンツへスキップ

ボディ - ネストされたモデル

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を使用してこれらの型アノテーションを宣言できます。💡

しかし、Python 3.9より前のバージョン (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
🤓 その他のバージョンとバリアント
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

型パラメータを持つlistを宣言する

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

  • Pythonのバージョンが3.9より低い場合は、typingモジュールから同等のバージョンをインポートしてください。
  • 内部の型を角括弧[]を使って「型パラメータ」として渡します。

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

my_list: list[str]

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

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スキーマ/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を直接扱っている場合、このようなエディターサポートは得られません。

しかし、それらについて心配する必要はありません。入力される辞書は自動的に変換され、出力も自動的に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モデルによって提供される最大限の柔軟性を持ちながら、コードをシンプル、短く、エレガントに保つことができます。

しかし、すべての利点もあります。

  • エディターサポート(どこでも補完!)
  • データ変換(別名:パース/シリアライズ)
  • データ検証
  • スキーマ文書
  • 自動ドキュメント