当サイトは、アフィリエイト広告を利用しています
FastAPIでAPIを実装する場合、リクエストの入力値はPydanticのモデルで検証することが多い。
たとえば、次のようなリクエストモデルを考える。
from pydantic import BaseModelclass Item(BaseModel):name: strquantity: int
このモデルでは、nameはstr、quantityはintとして検証される。
しかし、実務では型だけでは足りないことが多い。
たとえば、次のようなルールを付けたい場合がある。
nameは1文字以上20文字以下 quantityは1以上 passwordとpassword_confirmが一致しているか確認したい このような入力検証を実装するために、Pydanticには複数の仕組みが用意されている。
代表的なものは次のとおりである。
Field Annotated BeforeValidator AfterValidator WrapValidator PlainValidator @field_validator @model_validator この記事では、それぞれがどのようなものか、何が違うのか、実務ではどう使い分けるかを整理する。
Pydantic / FastAPIの入力検証は、次のように整理できる。
Pydantic / FastAPIの入力検証├─ Field│ └─ フィールドに制約やメタデータを付ける│ └─ 文字数、数値範囲、説明、デフォルト値など│├─ Annotated│ └─ 型にメタデータを付ける│ ├─ Fieldを型に結びつける│ └─ Validatorを型に結びつける│├─ AnnotatedパターンのValidator│ ├─ BeforeValidator│ │ └─ Pydanticの型検証前に動く│ │ └─ 入力値の正規化に使う│ ││ ├─ AfterValidator│ │ └─ Pydanticの型検証後に動く│ │ └─ 型確定後の追加チェックに使う│ ││ ├─ WrapValidator│ │ └─ Pydanticの検証処理を包む│ │ └─ 前処理と後処理をまとめて制御する│ ││ └─ PlainValidator│ └─ Pydanticの標準検証を置き換える│ └─ 独自の変換・検証に使う│└─ デコレータパターンのValidator├─ @field_validator│ └─ モデル内のフィールドを検証する│└─ @model_validator└─ モデル全体を検証する└─ 複数フィールドの関係チェックなどに使う
PydanticのフィールドValidatorには、次の2つの定義方法がある。
Annotatedパターン├─ BeforeValidator├─ AfterValidator├─ WrapValidator└─ PlainValidatorデコレータパターン├─ @field_validator(mode="before")├─ @field_validator(mode="after")├─ @field_validator(mode="wrap")└─ @field_validator(mode="plain")
それぞれは、次のように対応している。
BeforeValidator↔ @field_validator(mode="before")AfterValidator↔ @field_validator(mode="after")WrapValidator↔ @field_validator(mode="wrap")PlainValidator↔ @field_validator(mode="plain")
どちらのパターンでも、Pydantic本体の検証に対して、処理を実行するタイミングを指定できる。
違いは、Validatorをどこに定義するかである。
型にValidatorを結びつける→ Annotatedパターンモデルクラス内のフィールドにValidatorを定義する→ デコレータパターン
この記事では、型として切り出して再利用しやすいAnnotatedパターンと、
モデルクラス内に定義するデコレータパターンに分けて説明する。
Fieldとは何かFieldは、Pydanticのフィールドに制約やメタデータを付けるための仕組みである。
たとえば、文字列の長さや数値範囲を指定できる。
from pydantic import BaseModel, Fieldclass User(BaseModel):name: str = Field(min_length=1, max_length=20)age: int = Field(ge=0, le=120)
このモデルでは、次の制約が付く。
name
age
Fieldは、次のような宣言的に書ける制約に向いている。
たとえば、次のようにフィールドの説明も付けられる。
from pydantic import BaseModel, Fieldclass User(BaseModel):name: str = Field(min_length=1,max_length=20,description="ユーザー名",)
Fieldは、単純な制約やJSON Schema用のメタデータを書く場所である。
Annotatedとは何かAnnotatedは、Pythonの型ヒントにメタデータを付与するための仕組みである。
基本形は次のようになる。
Annotated[元の型, メタデータ]
Pydanticでは、FieldやValidatorを型に結びつけるために使える。
from typing import Annotatedfrom pydantic import FieldUserName = Annotated[str,Field(min_length=1, max_length=20),]
これは、次のように読める。
UserName├─ 型: str└─ メタデータ: Field(min_length=1, max_length=20)
Annotated自体が検証しているわけではない Annotatedは、型にメタデータを付けるための箱である このUserNameは、Pydanticモデルで使える。
from pydantic import BaseModelclass UserCreateRequest(BaseModel):name: UserName
このようにすると、nameはstrであり、かつ1文字以上20文字以下という制約を持つ。
Annotatedについては、次の記事でも詳しくまとめている。
FieldとAnnotatedの違いFieldは、制約やメタデータそのものである。
Annotatedは、それらを型に結びつけるための仕組みである。
次の2つは、どちらもnameに文字数制限を付けている。
from pydantic import BaseModel, Fieldclass UserCreateRequest(BaseModel):name: str = Field(min_length=1, max_length=20)
from typing import Annotatedfrom pydantic import BaseModel, FieldUserName = Annotated[str,Field(min_length=1, max_length=20),]class UserCreateRequest(BaseModel):name: UserName
違いは、再利用しやすさである。
Fieldをモデル内に直接書く場合、制約はそのフィールドに閉じる。
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で型として切り出すと、同じ制約を複数箇所で使い回せる。
UserName = Annotated[str,Field(min_length=1, max_length=20),]class UserCreateRequest(BaseModel):name: UserNameclass CompanyCreateRequest(BaseModel):contact_name: UserName
判断基準は次のようになる。
そのフィールドだけで使う制約→ Fieldをモデル内に直接書く複数箇所で使い回したい制約→ Annotatedで型として切り出す
ただし、Annotatedは再利用する場合にしか使えないわけではない。
再利用はAnnotatedを使う大きなメリットの1つである。
AnnotatedパターンのValidatorAnnotatedには、FieldだけでなくValidatorも付与できる。
代表的なものは次の4つである。
BeforeValidator AfterValidator WrapValidator PlainValidator これらは、Pydanticの検証フローのどこで処理するかが異なる。
入力値↓BeforeValidator↓Pydantic本体の型検証・型変換↓AfterValidator↓最終値
WrapValidatorは、Pydantic本体の検証処理を包み込む。
PlainValidatorは、Pydantic本体の型検証を行わず、独自の関数だけで変換・検証を完結させる。
BeforeValidatorとは?BeforeValidatorは、Pydanticが型変換・型検証を行う前に実行される。
主な用途は、入力値の正規化である。
たとえば、文字列の前後の空白を除去したい場合に使える。
from typing import Any, Annotatedfrom pydantic import BaseModel, BeforeValidatordef trim(v: Any) -> Any:if isinstance(v, str):return v.strip()return vTrimmedStr = Annotated[str,BeforeValidator(trim),]class UserCreateRequest(BaseModel):name: TrimmedStr
まず、trim関数を定義する。
次に、BeforeValidator(trim)をAnnotatedに指定する。
この場合、次の入力を受け取るとする。
{"name": " taro "}
処理の流れは次のようになる。
入力値 " taro "↓BeforeValidator(trim)↓"taro"↓Pydanticのstr検証↓最終値 "taro"
BeforeValidatorは型検証前に動く。
そのため、入力値がまだstrであるとは限らない。
次のように書くと、入力値によってはエラーになる可能性がある。
def trim(v: str) -> str:return v.strip()
入力値がNoneや数値だった場合、strip()を呼び出せないためである。
安全に処理する場合は、Anyで受け取り、必要に応じて型を確認する。
from typing import Anydef trim(v: Any) -> Any:if isinstance(v, str):return v.strip()return v
BeforeValidatorは、次のような型検証前に値を加工する処理に向いている。
Noneに変換する listに変換する listに変換する AfterValidatorとは?AfterValidatorは、Pydanticが型変換・型検証を行った後に実行される。
Validator関数に渡される時点では、基本的に指定した型として扱える。
たとえば、正の整数だけを許可する型を定義する。
from typing import Annotatedfrom pydantic import BaseModel, AfterValidatordef must_be_positive(v: int) -> int:if v <= 0:raise ValueError("正の整数である必要があります")return vPositiveInt = Annotated[int,AfterValidator(must_be_positive),]class Item(BaseModel):quantity: PositiveInt
まず、正の整数であることを確認する関数を定義する。
次に、AfterValidatorとしてAnnotatedに指定する。
処理の流れは次のようになる。
入力値↓Pydanticのint検証・変換↓AfterValidator(must_be_positive)↓最終値
AfterValidatorの時点では、vはintとして扱える。
そのため、BeforeValidatorよりも型安全に書きやすい。
AfterValidatorは、次のような処理に向いている。
入力値を整える場合はBeforeValidatorを使う。
型が決まった後に追加チェックする場合はAfterValidatorを使う。
BeforeValidatorとAfterValidatorを組み合わせるBeforeValidatorとAfterValidatorは組み合わせて使える。
たとえば、文字列について次のルールを作りたいとする。
この場合、次のように定義できる。
from typing import Any, Annotatedfrom pydantic import BaseModel, BeforeValidator, AfterValidatordef trim(v: Any) -> Any:if isinstance(v, str):return v.strip()return vdef not_empty(v: str) -> str:if v == "":raise ValueError("空文字は不可")return vNonEmptyStr = Annotated[str,BeforeValidator(trim),AfterValidator(not_empty),]class UserCreateRequest(BaseModel):name: NonEmptyStr
まず、BeforeValidator(trim)で前後の空白を除去する。
次に、Pydanticがstrとして検証する。
最後に、AfterValidator(not_empty)で空文字を禁止する。
処理の流れは次のようになる。
入力値 " taro "↓BeforeValidator(trim)↓"taro"↓Pydanticのstr検証↓AfterValidator(not_empty)↓最終値 "taro"
このNonEmptyStrは、通常のフィールドとして使える。
class UserCreateRequest(BaseModel):name: NonEmptyStr
また、listの要素にも使える。
class TagRequest(BaseModel):tags: list[NonEmptyStr]
この場合、tagsの各要素に対して、空白除去と空文字チェックが適用される。
WrapValidatorとは?WrapValidatorは、Pydantic本体の検証処理を包み込むValidatorである。
BeforeValidator
AfterValidator
一方、WrapValidatorはPydanticの検証処理全体を包み込む。
そのため、Pydantic本体の検証前後に処理を追加できる。
概念的には次のような流れになる。
WrapValidator開始↓前処理↓handler(v)でPydantic本体の検証↓後処理↓返却
例を示す。
from typing import Any, Annotatedfrom pydantic import BaseModel, ValidatorFunctionWrapHandler, WrapValidatordef wrap_trim(v: Any,handler: ValidatorFunctionWrapHandler,) -> str:# 前処理if isinstance(v, str):v = v.strip()# Pydantic本体の検証value = handler(v)# 後処理if value == "":raise ValueError("空文字は不可")return valueNonEmptyStr = Annotated[str,WrapValidator(wrap_trim),]class User(BaseModel):name: NonEmptyStr
処理の流れは次のとおりである。
handler(v)を呼び出す前に前処理を行う handler(v)によってPydantic本体の検証を実行する WrapValidatorは柔軟だが、その分、処理の流れが分かりにくくなりやすい。
前処理だけならBeforeValidator、後処理だけならAfterValidatorを使う方が読みやすい。
WrapValidatorは、前後処理やエラー処理などを1つの関数で制御したい場合に検討する。
PlainValidatorとは?PlainValidatorは、Pydanticの標準検証を使わず、自分の関数だけで変換・検証を完結させるValidatorである。
たとえば、独自に整数へ変換する型を作る場合を考える。
from typing import Any, Annotatedfrom pydantic import BaseModel, PlainValidatordef parse_int(v: Any) -> int:if isinstance(v, int):return vif isinstance(v, str) and v.isdecimal():return int(v)raise ValueError("整数に変換できません")MyInt = Annotated[int,PlainValidator(parse_int),]class Item(BaseModel):count: MyInt
PlainValidatorを使うと、Pydanticの通常の型検証を通さずに、Validator関数の戻り値がそのまま結果になる。
PlainValidatorの処理の流れは次のようになる。
PlainValidator開始↓入力値を受け取る↓自分で変換・検証する↓Pydantic本体の型検証は行わない↓Validator関数の戻り値を最終値として扱う↓返却
PlainValidatorは、Pydanticの標準検証を置き換える。
そのため、BeforeValidatorやAfterValidatorのように、Pydantic本体の型検証の前後に処理を差し込むものではない。
入力値をどのように解釈し、どの値を返すかをValidator関数側で決める必要がある。
PlainValidatorは強力だが、Pydanticの標準検証を利用しなくなるため、使いどころには注意が必要である。
実務では、次の順番で検討するとよい。
Fieldで表現できるか↓BeforeValidator / AfterValidatorで表現できるか↓WrapValidatorで制御する必要があるか↓PlainValidatorで標準検証を置き換える必要があるか
Pydanticでは、モデルクラス内にデコレータを使ってValidatorを定義することもできる。
代表的なものは次の2つである。
@field_validator @model_validator @field_validatorは、モデル内の特定フィールドに対して変換や検証を行う。
@model_validatorは、モデル全体に対して検証を行う。
特に、複数フィールドの関係を検証する場合に向いている。
@field_validatorとは?@field_validatorは、データモデルクラスの中にフィールドの検証処理を書くためのデコレータである。
from typing import Anyfrom pydantic import BaseModel, field_validatorclass UserCreateRequest(BaseModel):first_name: strlast_name: str@field_validator("first_name", "last_name", mode="before")@classmethoddef trim_name(cls, v: Any) -> Any:if isinstance(v, str):return v.strip()return v
この例では、first_nameとlast_nameの両方に対して、Pydantic本体の型検証前に空白除去を行っている。
@field_validatorは、次のような場合に向いている。
同じ検証ルールを複数のモデルで使い回したい場合は、Annotatedパターンの方が向いている。
NonEmptyStr = Annotated[str,BeforeValidator(trim),AfterValidator(not_empty),]
使い分けは次のようになる。
型として複数箇所で再利用したい→ Annotatedパターン特定モデル固有のフィールド検証として定義したい→ @field_validator
@model_validatorとは?@model_validatorは、モデル全体を検証するためのデコレータである。
特に、複数フィールドの関係を見る場合に使う。
たとえば、パスワード確認のようなケースを考える。
from typing_extensions import Selffrom pydantic import BaseModel, model_validatorclass UserCreateRequest(BaseModel):password: strpassword_confirm: str@model_validator(mode="after")def check_passwords_match(self) -> Self:if self.password != self.password_confirm:raise ValueError("パスワードが一致しません")return self
この検証では、passwordとpassword_confirmの両方を確認する必要がある。
このような検証は、単一フィールドの制約では表現しにくい。
モデル全体の整合性チェックとして扱う方が自然である。
@model_validatorは、次のような検証に向いている。
start_date <= end_date password == password_confirm emailまたはphoneのどちらか一方は必須 statusによって必須項目が変わる 単一の値だけを検証する場合は、Field、Annotatedパターン、または@field_validatorを使う。
複数フィールドの関係を検証する場合は、@model_validatorを使う。
Annotatedパターンとデコレータパターンのどちらでも、Validator関数を定義する。
Validator関数では、検証後の値を返す必要がある。
たとえば、次のように書く。
def not_empty(v: str) -> str:if v == "":raise ValueError("空文字は不可")return v
入力値を不正とする場合は、ValueErrorなどの例外を送出する。
問題がなければ、検証後の値を返す。
次のようにreturnを忘れると、正常な入力でもNoneが返される。
def not_empty(v: str) -> str:if v == "":raise ValueError("空文字は不可")
Validatorは、単に値をチェックする関数ではない。
入力値を受け取り、必要に応じて変換・検証し、その結果を返す関数である。
ここまで、Pydanticが提供する次の仕組みについて説明した。
Field Annotated BeforeValidator AfterValidator WrapValidator PlainValidator @field_validator @model_validator 実務でこれらを使い分ける場合は、機能名から選ぶのではなく、まず 検証ルールをどこに持たせるか を考えると整理しやすい。
その検証は特定のデータモデル内だけで使うか├─ はい│ ├─ 宣言的な単純な制約か│ │ └─ Field│ ││ ├─ 特定フィールドの変換・検証か│ │ └─ @field_validator│ ││ └─ 複数フィールドの関係を検証するか│ └─ @model_validator│└─ いいえ└─ 複数のモデルやフィールドで再利用したい└─ Annotatedパターンで制約付き型として定義する├─ 宣言的な制約│ └─ Field│├─ 型検証前に入力を整える│ └─ BeforeValidator│├─ 型検証後に追加チェックする│ └─ AfterValidator│├─ 検証処理の前後をまとめて制御する│ └─ WrapValidator│└─ Pydanticの標準検証を置き換える└─ PlainValidator
この図は、絶対的なルールではない。
Annotatedは再利用しない場合でも使えるが、
複数のモデルやフィールドで検証ルールを再利用したい場合に特に向いている。
検証ルールが特定のデータモデルでしか使われない場合は、モデル内に定義する。
文字数や数値範囲などの単純な制約
Field nameにだけ文字数制限を付ける場合は、Fieldをモデル内に直接書けばよい 特定フィールドの変換・検証
@field_validator @field_validatorを使う 複数フィールドの関係を検証
@model_validator @model_validatorを使う モデル固有の検証であれば、無理に外部へ切り出さず、モデル内に書く方が処理の所在を把握しやすい。
同じ検証ルールを複数のモデルやフィールドで使う場合は、Annotatedを使って型として切り出す。
定義した型は、複数箇所で再利用できる。
文字数や数値範囲などの宣言的な制約
Annotated + Field 型検証前の入力正規化
Annotated + BeforeValidator 型検証後の追加チェック
Annotated + AfterValidator 検証処理全体の制御
Annotated + WrapValidator Pydantic標準検証の置き換え
Annotated + PlainValidator 基本的には、入力値を整える場合はBeforeValidatorを使い、
型が確定した後に追加チェックする場合はAfterValidatorを使う。
WrapValidatorとPlainValidatorは検証フローを大きく制御するため、
BeforeValidatorやAfterValidatorでは対応できない場合に検討する。
Annotatedを使う目的は、単に書き方を変えることではない。
検証ルールを型に結びつけ、複数のモデルやフィールドで再利用しやすくすることである。
重要なのは、次のどちらに該当するかを最初に判断することである。