当サイトは、アフィリエイト広告を利用しています
FastAPIでは、入力値の検証にPydanticを使う。
リクエストボディを受け取る場合は、BaseModel を定義することが多い。
from fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI()class UserCreate(BaseModel):name: strage: int@app.post("/users")def create_user(user: UserCreate = Body(...)):return user
このような通常のAPI開発では、FastAPIがPydanticを使って自動で検証してくれる。
そのため、最初は BaseModel を理解していれば十分である。
しかし、実務では次のような場面がある。
これらは、必ずしも新しい BaseModel を作るほどではない。
しかし、型として正しいかは検証したい。
このときに使えるのが TypeAdapter である。
ただし、TypeAdapter は BaseModel や Annotated の代替ではない。
それぞれ役割が違う。
この記事では、TypeAdapter を単体で見るのではなく、BaseModel、Annotated、TypeAdapter の責務分離として整理する。
FastAPI / Pydanticの入力検証を理解するときは、次の3つを分けて考えると分かりやすい。
BaseModelは下記を持つ。
たとえば、ユーザー作成リクエストのように、複数のフィールドを持つ入力データは BaseModel で表現する。
from pydantic import BaseModel# データモデル定義class UserCreate(BaseModel):name: strage: int# インスタンス生成と同時に検証するuser = UserCreate(name="John Doe", age=30)
Annotatedは下記を持つ。
たとえばname に「1文字以上20文字以下」という制約を付けたい場合は、Annotated を使って型にルールを付けられる。
from typing import Annotatedfrom pydantic import Field# UserNameという制約付きの型を定義するUserName = Annotated[str, Field(min_length=1, max_length=20)]
Annotatedは、あくまで型定義のみ
Annotatedに関しては下記記事で詳しくまとめている
TypeAdapterは下記を持つ。
from pydantic import TypeAdapter# AnnotatedでUserNameという制約付きの型を定義するUserName = Annotated[str, Field(min_length=1, max_length=20)]# UserName型を検証するTypeAdapterを定義user_name_adapter = TypeAdapter(UserName)# 検証を行うname = user_name_adapter.validate_python("taro")
この整理を先に持っておくと、TypeAdapter の位置づけが分かりやすくなる。
TypeAdapterについて詳しく解説する
TypeAdapter は、任意の型に対してPydanticの検証機能を使うための仕組みである。
Pydantic公式ドキュメントでは、
TypeAdapter は BaseModel を作らずに型の検証、シリアライズ、JSON Schema生成を行うための機能 として説明されている。
基本構文は次の通りである。
from pydantic import TypeAdapteradapter = TypeAdapter(検証したい型)result = adapter.validate_python(入力値)
たとえば、dict[str, int] を検証する場合はこう書く。
from pydantic import TypeAdapteradapter = TypeAdapter(dict[str, int])data = adapter.validate_python({"apple": "100","banana": 200,})print(data)# {'apple': 100, 'banana': 200}
"100" は文字列だが、int に変換される。 バリデーションエラーにしたい場合
from pydantic import TypeAdapteradapter = TypeAdapter(dict[str, int])try:data = adapter.validate_python({"apple": "100","banana": 200,},strict=True)except ValidationError as e:print(e)
Pydanticはデフォルトでは、変換可能な値を目的の型に変換する。
厳密に型一致を求める場合は strict=True を使う。
TypeAdapterで検証でよく使う関数として
がある
validate_python() は、Pythonオブジェクトを検証するメソッドである。
from pydantic import TypeAdapteradapter = TypeAdapter(list[int])values = adapter.validate_python(["1", "2", 3])print(values)# [1, 2, 3]
外部APIのレスポンスを json.loads() した後、DBから取得した値、Redisから復元した値、CSVから読み込んだ値など、すでにPythonオブジェクトになっている値を検証するときに使う。
validate_json() は、JSON文字列を直接検証するメソッドである。
from pydantic import TypeAdapteradapter = TypeAdapter(list[int])values = adapter.validate_json('[1, "2", 3]')print(values)# [1, 2, 3]
json.loads() してから validate_python() に渡すのではなく、JSON文字列をそのままPydanticで検証できる。
Redis、メッセージキュー、ファイルなどからJSON文字列を取得する場合に使いやすい。
Annotated と TypeAdapter は組み合わせると使いやすい。
Annotated で制約付きの型を定義し、その型を TypeAdapter で検証する。
from typing import Annotatedfrom pydantic import Field, TypeAdapter# 型と制約定義UserName = Annotated[str, Field(min_length=1, max_length=20)]# 検証機作成user_name_adapter = TypeAdapter(UserName)# 検証name = user_name_adapter.validate_python("taro")print(name)# taro
この使い方のメリットは、同じ制約を複数の場所で使い回せることである。
たとえば、FastAPIのリクエストモデルでは UserName を型として使う。
from pydantic import BaseModelclass UserCreate(BaseModel):name: UserName
一方で、API以外の場所では TypeAdapter(UserName) を使って検証する。
def validate_user_name(value: str) -> UserName:return user_name_adapter.validate_python(value)
これにより、複数個所で同じルールを使い回せる。
Annotated はルールを定義する。 TypeAdapter はそのルールをAPI外でも使えるようにする。 この組み合わせは、実務でかなり使いやすい。
FastAPIでは、リクエストボディやクエリパラメータの検証は基本的に自動で行われる。
そのため、エンドポイントの引数で完結するなら TypeAdapter を使う必要は少ない。
TypeAdapter が役立つのは、FastAPIの自動検証が届かない場所である。
つまり、FastAPIにおける TypeAdapter は、リクエストモデルの代替ではない。
アプリ内部や外部境界で、Pydanticの検証を使うための道具である。
外部APIから次のようなデータが返るとする。
[{"id": 1, "name": "apple", "price": 120},{"id": 2, "name": "banana", "price": "180"}]
これをアプリ内部で使う前に検証したい。
from pydantic import BaseModel, TypeAdapter# データモデル定義class Product(BaseModel):id: intname: strprice: int# 検証機作成products_adapter = TypeAdapter(list[Product])def parse_products(response_data: object) -> list[Product]:# 検証とインスタンス生成return products_adapter.validate_python(response_data)
raw_data = [{"id": 1, "name": "apple", "price": 120},{"id": 2, "name": "banana", "price": "180"},]products = parse_products(raw_data)print(products)# [Product(id=1, name='apple', price=120), Product(id=2, name='banana', price=180)]
price の "180" は文字列だが、int に変換される。 このケースでは、list[Product] そのものを検証したい。
そのため、次のようなラッパーモデルを作らなくてもよい。
class ProductList(BaseModel):products: list[Product]
もちろん、API仕様として products というキーを持つレスポンスを表現したいなら、ラッパーモデルを作る意味はある。
しかし、単に配列そのものを検証したいだけなら、TypeAdapter(list[Product]) の方が自然である。
RedisやDBにJSON文字列を保存している場合、読み出した値はただの文字列である。
from pydantic import BaseModel, TypeAdapterclass UserSession(BaseModel):user_id: introles: list[str]session_adapter = TypeAdapter(UserSession)def load_session(json_text: str) -> UserSession:return session_adapter.validate_json(json_text)
json_text = '{"user_id": "123", "roles": ["admin", "editor"]}'session = load_session(json_text)print(session)# user_id=123 roles=['admin', 'editor']
user_id はJSON上では文字列だが、UserSession の定義に従って int に変換される。
Redis、DB、メッセージキュー、ファイルなど、アプリの外から入ってくる値は型が崩れている可能性がある。
このような外部境界で TypeAdapter を使うと、アプリ内部に値を入れる前に型を整えられる。
既存APIやフロントエンド都合で、次のようなクエリパラメータを受けたい場合がある。
GET /items?ids=1,2,3
FastAPIで list[int] のクエリパラメータを普通に使う場合は、一般的には次のような形式になる。
GET /items?ids=1&ids=2&ids=3
しかし、1,2,3 のようなカンマ区切りを受ける必要があるなら、Depends と TypeAdapter を組み合わせられる。
from fastapi import Depends, FastAPI, Queryfrom pydantic import TypeAdapterapp = FastAPI()ids_adapter = TypeAdapter(list[int])def get_ids(ids: str = Query("")) -> list[int]:if not ids:return []return ids_adapter.validate_python(ids.split(","))@app.get("/items")def list_items(ids: list[int] = Depends(get_ids)):return {"ids": ids}
レスポンスは下記になる
{"ids": [1, 2, 3]}
ids.split(",") の結果は ["1", "2", "3"] である。
それを TypeAdapter(list[int]) で検証することで、list[int] に変換している。
ただし、この例は「既存仕様に合わせるための実装」である。
新規APIであれば、FastAPIの標準的なクエリ表現に寄せた方がよい場合もある。
Annotated と TypeAdapter の組み合わせは、制約付き型を再利用したいときに便利である。
from typing import Annotatedfrom pydantic import BaseModel, Field, TypeAdapter# 型・制約定義UserName = Annotated[str, Field(min_length=1, max_length=20)]# 検証機定義user_name_adapter = TypeAdapter(UserName)# データモデル定義class UserCreate(BaseModel):name: UserNamedef validate_user_name(value: str) -> UserName:return user_name_adapter.validate_python(value)
これにより、APIでもAPI以外でも同じ UserName のルールを使える。
name = validate_user_name("taro")print(name)# taro
annotatedで定義した型を
で再利用することでバリデーションルールが分散しにくい。
で同じルールで検証できる
TypeAdapter は便利だが、何でも TypeAdapter にすればよいわけではない。
リクエストボディ全体を表現したいなら、通常は BaseModel を使う方がよい。
from pydantic import BaseModelclass UserCreate(BaseModel):name: strage: int
これは UserCreate という意味のある入力データである。
このような場合に、あえて dict[str, object] を TypeAdapter で検証するより、BaseModel として定義した方が分かりやすい。
このようなものは、BaseModel の方が向いている。
一方で、次のようなものは TypeAdapter が向いている。
FastAPIの自動検証で足りるなら無理に使わない
エンドポイントの型アノテーションで済むなら、それが一番シンプルである。
TypeAdapter は、Pydantic v2で BaseModelを作らずに任意の型を検証するための機能 である。
FastAPIで TypeAdapter を理解するときに重要なのは、次の2点である。
Annotated で制約付きの型を作っておけば、FastAPIのリクエストモデルでは型として使える。
さらに、バッチ処理、CLI、外部データ取り込みなどでは TypeAdapter を使って同じルールで検証できる。
つまり、TypeAdapter はFastAPIのリクエストモデルを置き換えるものではない。
FastAPIの自動検証が届かない場所でも、同じ型ルールを使って値を検証するための仕組みである。