コンテンツにスキップ

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

FastAPI を使用すると、深くネストされた任意のモデルを定義、検証、文書化、使用することができます(Pydanticのおかげです)。

リストのフィールド

属性をサブタイプとして定義することができます。例えば、Pythonのlistは以下のように定義できます:

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の標準の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のようなタイプパラメータ(内部の型)を持つ型を宣言するには:

  • typingモジュールからそれらをインストールします。
  • 角括弧([])を使って「タイプパラメータ」として内部の型を渡します:
from typing import List

my_list: List[str]

型宣言の標準的なPythonの構文はこれだけです。

内部の型を持つモデルの属性にも同じ標準の構文を使用してください。

そのため、以下の例ではtagsを具体的な「文字列のリスト」にすることができます:

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があります。

そのため、以下のように、Setをインポートしてstrsetとしてtagsを宣言することができます:

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モデルである可能性があります。

そのため、特定の属性名、型、バリデーションを指定して、深くネストしたJSONobjectを宣言することができます。

すべては、任意のネストにされています。

サブモデルの定義

例えば、Imageモデルを定義することができます:

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 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 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 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 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のリストであり、オプションのImageのリストを持っていることに注目してください。

純粋なリストのボディ

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

images: List[Image]

以下のように:

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モデルの場合のように)。

これは、まだ知らないキーを受け取りたいときに便利だと思います。


他にも、intのように他の型のキーを持ちたい場合などに便利です。

それをここで見ていきましょう。

この場合、intのキーとfloatの値を持つものであれば、どんなdictでも受け入れることができます:

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モデルが提供する最大限の柔軟性を持ちながら、コードをシンプルに短く、エレガントに保つことができます。

以下のような利点があります:

  • エディタのサポート(どこでも補完!)
  • データ変換(別名:構文解析・シリアライズ)
  • データの検証
  • スキーマ文書
  • 自動文書化