当サイトは、アフィリエイト広告を利用しています
FastAPI で API を実装していると、次のようなデータモデルを書くことがある。
from pydantic import BaseModelclass Order(BaseModel):order_id: stritems: list[str] = []
この items は「文字列のリスト」である。
そのため、この定義で検証されるのは主に次の内容である。
つまり、items がリストであり、その中身が文字列であることは検証される。
しかし、各文字列の長さや空文字の扱いまでは、この定義だけでは表現できない。
たとえば、商品名のリストであれば、次のような制約をかけたいことがある。
このような 型だけでは表現できない追加情報 を扱うときに使えるのが Annotated である。
ただし、Annotated が使われる場面は list[str] の要素バリデーションだけではない。
FastAPI / Pydantic では、型に対して制約や API 仕様などのメタデータを付与するために使われる。
この記事では、FastAPI / Pydantic における Annotated の基本的な使い方を整理する。
その具体例の1つとして、list[str] の要素に制約をかける方法も扱う。
Annotated は、Python の型ヒントにメタデータ(制約条件)を付与するための仕組みである。
PydanticはAnnotated を使った付与された制約条件を解釈し、バリデーションを実行する
基本形は次のようになる。
Annotated[元の型, メタデータ]
たとえば、Pydantic の Field と組み合わせる場合は、次のように書く。
from typing import Annotatedfrom pydantic import FieldItemName = Annotated[str,Field(min_length=1, max_length=20),]
まず、Annotated の第1引数に元の型を書く。
次に、第2引数以降にメタデータを書く。
上の例では、str が元の型であり、Field(min_length=1, max_length=20) がメタデータである。
概念的には、次のような意味になる。
ItemName├─ 型: str└─ 制約: 1文字以上20文字以下
つまり、ItemName は単なる str ではなく、Pydantic が読み取れる制約情報を持った文字列型として扱える。
ここで重要なのは、Annotated 自体がバリデーションを実行するわけではないという点だ。
Annotated は、型にメタデータを添えるための仕組みにすぎない。
そのメタデータを読み取り、実際にバリデーションするのは Pydantic である。
Field は、Pydantic に対して制約やメタデータを伝えるためのものだ。
Field(min_length=1, max_length=20)
これは型そのものではない。
str や int のような型ではなく、Pydantic が読むための追加情報である。
Annotated を使うことで、型と Field を結びつけられる。
ItemName = Annotated[str,Field(min_length=1, max_length=20),]
概念的には次のようになる。
Annotated├─ 元の型│ └─ str└─ メタデータ└─ Field(min_length=1, max_length=20)
つまり、Annotated は型と制約をひとまとまりにする仕組みである。
ただし、繰り返しになるが、Annotated が直接バリデーションしているわけではない。
Field に書かれた制約を Pydantic が読み取り、検証を行う。
この関係を理解しておくと、Annotated を単なる記法としてではなく、型とメタデータを結びつける仕組みとして捉えやすくなる。
Annotated は
のように使う
Annotated を使うと、制約付きの型を定義できる。
たとえば、ユーザー名として使う文字列に、次のような制約を付けたいとする。
1文字以上20文字以下
この場合、次のように定義できる。
from typing import Annotatedfrom pydantic import FieldUserName = Annotated[str,Field(min_length=1, max_length=20),]
まず、元の型として str を指定する。
次に、Field(min_length=1, max_length=20) を指定し、文字列長の制約を付ける。
この UserName は、Pydantic モデルのフィールドとして使える。
from pydantic import BaseModelclass UserCreateRequest(BaseModel):name: UserName
このようにすると、name は文字列であり、かつ1文字以上20文字以下である必要がある。
Annotated を使うことで、型と制約を1つの定義として扱える。
Annotated の大きなメリットは、制約付きの型を再利用できることである。
UserName = Annotated[str,Field(min_length=1, max_length=20),]
この UserName は、複数のモデルで使える。
class UserCreateRequest(BaseModel):name: UserNameclass CompanyCreateRequest(BaseModel):contact_name: UserName
このようにすると、同じ制約を複数箇所に書かずに済む。
もし Field(min_length=1, max_length=20) を各モデルに直接書くと、次のようになる。
class UserCreateRequest(BaseModel):name: str = Field(min_length=1, max_length=20)class CompanyCreateRequest(BaseModel):contact_name: str = Field(min_length=1, max_length=20)
この書き方でも動作する。
しかし、同じ制約が増えると管理しづらくなる。
Annotated を使って制約付き型として切り出すと、ルールを1箇所にまとめられる。
UserName = Annotated[str,Field(min_length=1, max_length=20),]
バリデーションルールをモデルごとに書くのではなく、型として切り出せる。
これが Annotated の強みである。
Annotated の使い方を理解する具体例として、リストの要素に制約をかけるケースを見る。
たとえば、商品名のリストを受け取る API を考える。
from typing import Annotatedfrom pydantic import BaseModel, FieldItemName = Annotated[str,Field(min_length=1, max_length=20),]class Order(BaseModel):order_id: stritems: list[ItemName] = []
まず、ItemName という制約付きの文字列型を定義する。
次に、items の型を list[ItemName] とする。
この定義では、items はリストである。
そして、そのリストの各要素は ItemName として検証される。
構造としては次のようになる。
Order├─ order_id: str└─ items: list[ItemName]└─ ItemName = str + 文字数制約
つまり、items のリスト全体ではなく、リストの中の1つ1つの文字列に対して制約が適用される。
たとえば、次の入力は通る。
{"order_id": "A001","items": ["apple", "banana"]}
一方で、次のような入力はエラーになる。
{"order_id": "A001","items": ["", "very-very-very-long-item-name"]}
このように、Annotated で定義した制約付き型は、通常のフィールドだけでなく、list の要素としても使える。
FastAPIではデータはデータモデルとして扱うのが基本となる。
つまり、list[str] の要素に制約をかける方法としては、Annotated 以外にも子データモデルを定義する方法がある。
ここでは、Annotated と子データモデルの違いを見る。
どちらが正しいという話ではなく、データをどう表現したいかによって使い分ける。
まず、子データモデルを定義する方法を見る。
from pydantic import BaseModel, Fieldclass Item(BaseModel):name: str = Field(min_length=1, max_length=20)class Order(BaseModel):order_id: stritems: list[Item] = []
この場合、リクエスト JSON は次のような形になる。
{"order_id": "A001","items": [{ "name": "apple" },{ "name": "banana" }]}
items の中身は文字列ではなく、Item オブジェクトである。
構造は次のようになる。
Order└─ items: list[Item]└─ Item└─ name: str
この方法は、商品を単なる文字列ではなく、意味のあるオブジェクトとして扱いたい場合に向いている。
たとえば、将来的に次のような属性が増える可能性があるなら、子データモデルにする方が自然だ。
from pydantic import BaseModel, Fieldclass Item(BaseModel):name: str = Field(min_length=1, max_length=20)item_id: intprice: intclass Order(BaseModel):order_id: stritems: list[Item] = []
Annotated を使う次に、Annotated を使う方法を見る。
from typing import Annotatedfrom pydantic import BaseModel, FieldItemName = Annotated[str,Field(min_length=1, max_length=20),]class Order(BaseModel):order_id: stritems: list[ItemName] = []
この場合、リクエスト JSON は次のような形になる。
{"order_id": "A001","items": ["apple", "banana"]}
items の中身は、あくまで文字列である。
ただし、その文字列には制約が付いている。
構造は次のようになる。
Order└─ items: list[ItemName]└─ ItemName = str + 制約
Annotated を使う場合、JSON の構造は list[str] に近いまま維持できる。
一方で、子モデルを使う場合は、リストの要素がオブジェクトになる。
この違いは、Annotated の使い方を考えるうえで重要である。
Annotated は、値そのものに追加情報を付けるための仕組みであり、データ構造そのものを増やすものではない。
子データモデルと Annotated の違いは、次のように整理できる。
データ構造
Annotated は、リストの要素をプリミティブ値として扱う 型の例
list[Item] のように定義する Annotated では list[ItemName] のように定義する JSON の形
{ "name": "apple" } のようなオブジェクトの配列になる Annotated では "apple" のような文字列の配列になる 表現力
Annotated は、単純な値に制約を付けるためシンプルである 将来拡張
item_id や quantity などの属性を追加しやすい Annotated は、値そのものに制約を付ける用途が中心なので、属性追加には向かない 向いている用途
Annotated は、制約付きの単純値に向いている Annotated の実務での使い分けAnnotated は便利だが、すべてのバリデーションを Annotated に寄せればよいわけではない。
Annotated が向いているものAnnotated は、単一の値に閉じた制約に向いている。
たとえば、次のようなものだ。
ItemName = Annotated[str,Field(min_length=1, max_length=20),]
または、FastAPI のクエリパラメータ制約にも使える。
SearchQuery = Annotated[str | None,Query(min_length=1, max_length=50),]
これらは、いずれも「その値自身」に対するルールである。
このようなケースでは、Annotated を使うと型とルールを近い場所にまとめられる。
子データモデルは、データが構造を持つ場合に向いている。
例を示す。
class Item(BaseModel):item_id: strname: strquantity: int
このように、複数の属性を持つデータは、Annotated ではなくモデルとして定義する方が分かりやすい。
複数フィールドをまたぐ検証は、Annotated だけで表現するより、モデル全体のバリデーション(model_validator)
として扱う方が自然である。
Annotated は基本的に「その値自身」に対するルールに向いている。
複数の値の関係を見る場合は、モデル全体の責務として分けて考える。
Annotatedを使うAnnotated は、Pydantic モデルのフィールドだけでなく、FastAPI の Query とも組み合わせられる。
クエリパラメータ q に文字数制限を付けたい場合を考える。
from typing import Annotatedfrom fastapi import FastAPI, Queryapp = FastAPI()@app.get("/items")async def get_items(q: Annotated[str | None, Query(min_length=1, max_length=50)] = None,):return {"q": q}
まず、q の型として str | None を指定する。
次に、Query(min_length=1, max_length=50) を指定し、クエリパラメータとしての制約を付ける。
この場合、役割は次のように分かれる。
str | None→ Pythonとしての型Query(min_length=1, max_length=50)→ FastAPIが読むメタデータ= None→ Pythonとしてのデフォルト値
Query は、クエリパラメータとしての扱い方や制約を FastAPI に伝えるためのメタデータである。
Annotated を使うことで、型と FastAPI 用のメタデータを分けて表現できる。
パスパラメータでも Annotated を使える。
たとえば、item_id は1以上の整数である、という制約を付けたい場合を考える。
from typing import Annotatedfrom fastapi import FastAPI, Pathapp = FastAPI()@app.get("/items/{item_id}")async def get_item(item_id: Annotated[int, Path(ge=1)],):return {"item_id": item_id}
まず、item_id の型として int を指定する。
次に、Path(ge=1) を指定し、パスパラメータとしての制約を付ける。
この item_id は、次のような意味になる。
リクエストボディにも Annotated を使える。
from typing import Annotatedfrom fastapi import Body, FastAPIfrom pydantic import BaseModelapp = FastAPI()class UserCreateRequest(BaseModel):name: stremail: str@app.post("/users")async def create_user(request: Annotated[UserCreateRequest, Body()],):return request
この場合、request は UserCreateRequest 型のリクエストボディとして扱われる。
また、Body(embed=True) のような指定もできる。
@app.post("/users")async def create_user(request: Annotated[UserCreateRequest, Body(embed=True)],):return request
Body(embed=True) を使うと、リクエスト JSON の形が変わる。
通常は次のような形で受け取る。
{"name": "taro","email": "taro@example.com"}
embed=True の場合は、次のように request というキーで包まれた形になる。
{"request": {"name": "taro","email": "taro@example.com"}}
このように、Body はリクエストボディとしての扱い方を FastAPI に伝えるメタデータである。
いくつか注意点をまとめておく
Annotated 自体がバリデーションしているわけではないAnnotated は、型にメタデータを付ける仕組みである。
実際にバリデーションするのは、FastAPI や Pydantic である。
ItemName = Annotated[str,Field(min_length=1, max_length=20),]
この場合、Annotated が文字数をチェックしているのではない。
Field(min_length=1, max_length=20) というメタデータを Pydantic が読み取り、Pydantic が検証している。
Field や Query は型ではないField(...) や Query(...) は型ではない。
Annotated[str, Field(min_length=1)]Annotated[str, Query(min_length=1)]
どちらも、str という型に追加情報を付けているだけである。
Annotated の第1引数に型を書く。
第2引数以降に、Pydantic や FastAPI が読むメタデータを書く。
この構造を意識すると、Annotated の読み方が分かりやすくなる。
Annotated に詰め込みすぎないAnnotated は便利だが、何でも詰め込むと読みにくくなる。
たとえば、制約が増えすぎると、定義元を見ないと何が起きるか分かりにくい。
UserName = Annotated[str,Field(min_length=3,max_length=30,pattern=r"^[a-zA-Z0-9_]+$",),]
この程度ならまだよいが、制約や意味が複雑になりすぎる場合は、別の設計を検討した方がよい。
Annotated は便利な道具だが、すべての設計を置き換えるものではない。
型にメタデータを付けるだけで済むのか、モデルとして構造化すべきなのか、モデル全体で検証すべきなのかを分けて考える必要がある。
Annotated は、Python の型ヒントにメタデータを付与するための仕組みである。
FastAPI / Pydantic では、Field、Query、Path、Body などと組み合わせることで、型に対して制約や API 仕様を付けられる。
また、Annotated は制約付き型の再利用にも向いている。
同じ制約を複数箇所で使う場合、Field(...) を各フィールドに直接書くのではなく、UserName や ItemName のような型として切り出すことで、ルールを1箇所にまとめられる。
一方で、Annotated はすべての設計を置き換えるものではない。
値そのものに制約を付けたい場合は Annotated が向いているが、複数の属性を持つデータは子データモデルとして定義する方が自然である。
また、複数フィールドをまたぐ検証は、モデル全体のバリデーションとして扱う方が分かりやすい。
Annotated は、単なる記法の違いではない。
型、制約、API 仕様を整理して表現するための仕組みである。