当サイトは、アフィリエイト広告を利用しています

【FastAPI】Annotatedで制約付き型を定義する方法

作成日:2026月06月20日
更新日:2026年06月20日

FastAPI で API を実装していると、次のようなデータモデルを書くことがある。

データモデル
from pydantic import BaseModel
class Order(BaseModel):
order_id: str
items: list[str] = []

この items は「文字列のリスト」である。
そのため、この定義で検証されるのは主に次の内容である。

  • items は list である
  • items の各要素は str である

つまり、items がリストであり、その中身が文字列であることは検証される。
しかし、各文字列の長さや空文字の扱いまでは、この定義だけでは表現できない。

たとえば、商品名のリストであれば、次のような制約をかけたいことがある。

  • 空文字は禁止したい
  • 1文字以上20文字以下にしたい
  • 特定の形式だけ許可したい

このような 型だけでは表現できない追加情報 を扱うときに使えるのが Annotated である。

ただし、Annotated が使われる場面は list[str] の要素バリデーションだけではない。
FastAPI / Pydantic では、型に対して制約や API 仕様などのメタデータを付与するために使われる。

この記事では、FastAPI / Pydantic における Annotated の基本的な使い方を整理する。
その具体例の1つとして、list[str] の要素に制約をかける方法も扱う。

Annotatedとは何か?

Annotated は、Python の型ヒントにメタデータ(制約条件)を付与するための仕組みである。
PydanticはAnnotated を使った付与された制約条件を解釈し、バリデーションを実行する

基本形は次のようになる。

Annotatedの基本形
Annotated[元の型, メタデータ]

たとえば、Pydantic の Field と組み合わせる場合は、次のように書く。

FieldとAnnotatedの例
from typing import Annotated
from pydantic import Field
ItemName = Annotated[
str,
Field(min_length=1, max_length=20),
]

まず、Annotated の第1引数に元の型を書く。
次に、第2引数以降にメタデータを書く。

上の例では、str が元の型であり、Field(min_length=1, max_length=20) がメタデータである。

概念的には、次のような意味になる。

Annotated
ItemName
├─ 型: str
└─ 制約: 1文字以上20文字以下

つまり、ItemName は単なる str ではなく、Pydantic が読み取れる制約情報を持った文字列型として扱える。

ここで重要なのは、Annotated 自体がバリデーションを実行するわけではないという点だ。

Annotated は、型にメタデータを添えるための仕組みにすぎない。
そのメタデータを読み取り、実際にバリデーションするのは Pydantic である。

AnnotatedとFieldの関係

Field は、Pydantic に対して制約やメタデータを伝えるためのものだ。

Fieldの例
Field(min_length=1, max_length=20)

これは型そのものではない。
strint のような型ではなく、Pydantic が読むための追加情報である。

Annotated を使うことで、型と Field を結びつけられる。

AnnotatedとFieldの組み合わせ
ItemName = Annotated[
str,
Field(min_length=1, max_length=20),
]

概念的には次のようになる。

Annotated
Annotated
├─ 元の型
│ └─ str
└─ メタデータ
└─ Field(min_length=1, max_length=20)

つまり、Annotated は型と制約をひとまとまりにする仕組みである。

ただし、繰り返しになるが、Annotated が直接バリデーションしているわけではない。
Field に書かれた制約を Pydantic が読み取り、検証を行う。

この関係を理解しておくと、Annotated を単なる記法としてではなく、型とメタデータを結びつける仕組みとして捉えやすくなる。

Annotatedの使い方

Annotated

  • 制約付き型を定義する
  • 制約付き型を再利用する

のように使う

制約付き型を定義する

Annotated を使うと、制約付きの型を定義できる。

たとえば、ユーザー名として使う文字列に、次のような制約を付けたいとする。

txt
1文字以上
20文字以下

この場合、次のように定義できる。

制約付き文字列型
from typing import Annotated
from pydantic import Field
UserName = Annotated[
str,
Field(min_length=1, max_length=20),
]

まず、元の型として str を指定する。
次に、Field(min_length=1, max_length=20) を指定し、文字列長の制約を付ける。

この UserName は、Pydantic モデルのフィールドとして使える。

制約付き型をモデルで使う
from pydantic import BaseModel
class 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: UserName
class 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箇所にまとめられる。

ルールを1箇所にまとめる
UserName = Annotated[
str,
Field(min_length=1, max_length=20),
]

バリデーションルールをモデルごとに書くのではなく、型として切り出せる。
これが Annotated の強みである。


Annotatedの具体例:list の各要素に制約をかける

Annotated の使い方を理解する具体例として、リストの要素に制約をかけるケースを見る。

たとえば、商品名のリストを受け取る API を考える。

listの要素に制約をかける
from typing import Annotated
from pydantic import BaseModel, Field
ItemName = Annotated[
str,
Field(min_length=1, max_length=20),
]
class Order(BaseModel):
order_id: str
items: 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"]
}
  • ItemNameの制約条件に一致してるため

一方で、次のような入力はエラーになる。

エラーになるリクエスト例
{
"order_id": "A001",
"items": ["", "very-very-very-long-item-name"]
}
  • ItemNameの制約条件に一致していない

このように、Annotated で定義した制約付き型は、通常のフィールドだけでなく、list の要素としても使える。

子データモデルを使う方法との違い

FastAPIではデータはデータモデルとして扱うのが基本となる。

つまり、list[str] の要素に制約をかける方法としては、Annotated 以外にも子データモデルを定義する方法がある。

ここでは、Annotated と子データモデルの違いを見る。
どちらが正しいという話ではなく、データをどう表現したいかによって使い分ける。

方法1:子データモデルを定義する

まず、子データモデルを定義する方法を見る。

子モデルを使う例
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str = Field(min_length=1, max_length=20)
class Order(BaseModel):
order_id: str
items: list[Item] = []

この場合、リクエスト JSON は次のような形になる。

子モデルを使う場合のJSON
{
"order_id": "A001",
"items": [
{ "name": "apple" },
{ "name": "banana" }
]
}

items の中身は文字列ではなく、Item オブジェクトである。

構造は次のようになる。

bash
Order
└─ items: list[Item]
└─ Item
└─ name: str

この方法は、商品を単なる文字列ではなく、意味のあるオブジェクトとして扱いたい場合に向いている。

たとえば、将来的に次のような属性が増える可能性があるなら、子データモデルにする方が自然だ。

プロパティ増える
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str = Field(min_length=1, max_length=20)
item_id: int
price: int
class Order(BaseModel):
order_id: str
items: list[Item] = []

方法2:Annotated を使う

次に、Annotated を使う方法を見る。

Annotatedを使う例
from typing import Annotated
from pydantic import BaseModel, Field
ItemName = Annotated[
str,
Field(min_length=1, max_length=20),
]
class Order(BaseModel):
order_id: str
items: list[ItemName] = []

この場合、リクエスト JSON は次のような形になる。

Annotatedを使う場合の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_idquantity などの属性を追加しやすい
    • Annotated は、値そのものに制約を付ける用途が中心なので、属性追加には向かない
  • 向いている用途

    • 子データモデルは、構造を持つデータに向いている
    • Annotated は、制約付きの単純値に向いている

子データモデルと Annotated の実務での使い分け

Annotated は便利だが、すべてのバリデーションを Annotated に寄せればよいわけではない。

Annotated が向いているもの

Annotated は、単一の値に閉じた制約に向いている。

  • 文字列の長さ制限
  • 空文字禁止
  • 数値の範囲制限
  • ID の正数チェック
  • list の各要素の制約
  • 複数モデルで再利用したい入力ルール
  • Query / Path / Body の API 仕様

たとえば、次のようなものだ。

文字列長の制約
ItemName = Annotated[
str,
Field(min_length=1, max_length=20),
]

または、FastAPI のクエリパラメータ制約にも使える。

Queryの制約
SearchQuery = Annotated[
str | None,
Query(min_length=1, max_length=50),
]

これらは、いずれも「その値自身」に対するルールである。
このようなケースでは、Annotated を使うと型とルールを近い場所にまとめられる。

子データモデルが向いているもの

子データモデルは、データが構造を持つ場合に向いている。

  • 商品が name 以外の属性を持つ
  • 住所が postal_code / prefecture / city を持つ
  • ユーザーが id / name / email を持つ
  • API設計上、独立したデータモデルとして扱いたい

例を示す。

子モデルが向いている例
class Item(BaseModel):
item_id: str
name: str
quantity: int

このように、複数の属性を持つデータは、Annotated ではなくモデルとして定義する方が分かりやすい。

複数フィールドをまたぐ検証は別で考える

複数フィールドをまたぐ検証は、Annotated だけで表現するより、モデル全体のバリデーション(model_validator)
として扱う方が自然である。

  • start_date < end_date
  • password == password_confirm
  • email または phone のどちらか一方は必須
  • status によって required な項目が変わる

Annotated は基本的に「その値自身」に対するルールに向いている。
複数の値の関係を見る場合は、モデル全体の責務として分けて考える。

FastAPIでAnnotatedを使う

Annotated は、Pydantic モデルのフィールドだけでなく、FastAPI の Query とも組み合わせられる。

FastAPI の Query で使う

クエリパラメータ q に文字数制限を付けたい場合を考える。

QueryでAnnotatedを使う例
from typing import Annotated
from fastapi import FastAPI, Query
app = 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 用のメタデータを分けて表現できる。

FastAPI の Path で使う

パスパラメータでも Annotated を使える。

たとえば、item_id は1以上の整数である、という制約を付けたい場合を考える。

PathでAnnotatedを使う例
from typing import Annotated
from fastapi import FastAPI, Path
app = 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 は、次のような意味になる。

  • item_id は int
  • パスパラメータとして受け取る
  • 1以上である必要がある

FastAPI の Body で使う

リクエストボディにも Annotated を使える。

BodyでAnnotatedを使う例
from typing import Annotated
from fastapi import Body, FastAPI
from pydantic import BaseModel
app = FastAPI()
class UserCreateRequest(BaseModel):
name: str
email: str
@app.post("/users")
async def create_user(
request: Annotated[UserCreateRequest, Body()],
):
return request

この場合、requestUserCreateRequest 型のリクエストボディとして扱われる。

また、Body(embed=True) のような指定もできる。

Body(embed=True)の例
@app.post("/users")
async def create_user(
request: Annotated[UserCreateRequest, Body(embed=True)],
):
return request

Body(embed=True) を使うと、リクエスト JSON の形が変わる。

通常は次のような形で受け取る。

通常のリクエストBody
{
"name": "taro",
"email": "taro@example.com"
}

embed=True の場合は、次のように request というキーで包まれた形になる。

embed=Trueの場合のリクエストBody
{
"request": {
"name": "taro",
"email": "taro@example.com"
}
}

このように、Body はリクエストボディとしての扱い方を FastAPI に伝えるメタデータである。

注意点

いくつか注意点をまとめておく

Annotated 自体がバリデーションしているわけではない

Annotated は、型にメタデータを付ける仕組みである。

実際にバリデーションするのは、FastAPI や Pydantic である。

Annotatedの役割
ItemName = Annotated[
str,
Field(min_length=1, max_length=20),
]

この場合、Annotated が文字数をチェックしているのではない。
Field(min_length=1, max_length=20) というメタデータを Pydantic が読み取り、Pydantic が検証している。

FieldQuery は型ではない

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 では、FieldQueryPathBody などと組み合わせることで、型に対して制約や API 仕様を付けられる。

また、Annotated は制約付き型の再利用にも向いている。
同じ制約を複数箇所で使う場合、Field(...) を各フィールドに直接書くのではなく、UserNameItemName のような型として切り出すことで、ルールを1箇所にまとめられる。

一方で、Annotated はすべての設計を置き換えるものではない。
値そのものに制約を付けたい場合は Annotated が向いているが、複数の属性を持つデータは子データモデルとして定義する方が自然である。
また、複数フィールドをまたぐ検証は、モデル全体のバリデーションとして扱う方が分かりやすい。

Annotated は、単なる記法の違いではない。
型、制約、API 仕様を整理して表現するための仕組みである。

関連記事

新着記事

top