Pydanticのfield_validatorは、型ヒントとFieldの制約だけでは表せない入力ルールを、メソッドとしてモデルに足すデコレータです。複数のフィールドを突き合わせる検証に使うのはmodel_validatorです。この記事では、2026年11月時点の最新版pydantic 2.14.3を前提に、mode(before・after・plain・wrap)で受け取る値がどう変わるか、ValidationInfoで他のフィールドを参照する方法、複数のバリデータが動く順番を、コードで示します。v1の@validatorと@root_validatorからの書き換え表も載せています。
まとめ:field_validatorとmodel_validatorの使い分けの結論
1つのフィールドの値を確かめる、または整えるならfield_validator、複数のフィールドの関係を確かめるならmodel_validator(mode="after")を使います。迷ったら、既定のmode="after"から書き始めるのが安全です。型変換が済んだ値を受け取るため、入力の型を場合分けせずに済みます。
範囲や長さ、正規表現で表せる条件は、バリデータではなくFieldの引数に書きます。バリデータの条件はJSON Schemaに出力されないため、API仕様書に載らないからです。v1の@validatorは2.14.3でも警告付きで動きますが、values引数やeach_itemは書き換えが必要になります。
field_validatorの基本形と値を返す・エラーを送出する書き方
まずはデコレータの書式と、検証に失敗したときの例外の選び方を押さえます。PyPIのpydanticでは、2026年11月14日時点の最新が2.14.3(11月5日公開・Python 3.10以上)です。
デコレータの引数とclassmethodを省略できる条件と戻り値の注意
v2.14.3のfunctional_validators.pyでは、field_validatorの引数は対象のフィールド名(1つ以上)、mode(既定は"after")、check_fields、json_schema_input_typeの4種類です。
from pydantic import BaseModel, field_validator
class SignupForm(BaseModel):
user_name: str
display_name: str
@field_validator("user_name", "display_name")
@classmethod
def strip_and_check(cls, v: str) -> str:
v = v.strip() # 前後の空白を落としてから判定する
if not v:
raise ValueError("空欄にはできません")
return v # 返した値がフィールドに入る
print(SignupForm(user_name=" taro ", display_name=" 山田 太郎"))
# user_name='taro' display_name='山田 太郎'
押さえる点は2つです。1つ目は戻り値で、バリデータが返した値がそのままフィールドに入ります。returnを書き忘れると、エラーにはならず値がNoneに置き換わります。
2つ目は@classmethodです。内部の判定処理は、第1引数の名前がclsなら自動でclassmethodとして扱います。省略しても動きますが、公式の例にそろえて明示しておく方が読み手に親切です。第1引数をselfにすると、クラスを定義した時点でPydanticUserErrorになります。
ValueErrorとPydanticCustomErrorの使い分けと種別
公式のValidatorsの解説によると、検証エラーとして扱われる例外は3種類です。
| 例外 | エラー種別 | 注意点 |
|---|---|---|
| ValueError | value_error | 文頭にValue errorが付く |
| assert文 | assertion_error | -Oで実行すると無効 |
| PydanticCustomError | 任意の名前 | 文言も種別も指定できる |
本番の入力検証にassertは使いません。Pythonを-O付きで起動するとassert文ごと消え、検証が素通りになるためです。利用者に見せるメッセージと、プログラムで分岐するための種別を分けたいならPydanticCustomErrorを選びます。
from pydantic import BaseModel, field_validator
from pydantic_core import PydanticCustomError
class Member(BaseModel):
kana: str
@field_validator("kana")
@classmethod
def only_katakana(cls, v: str) -> str:
if not all("ァ" <= ch <= "ヶ" or ch in "ー " for ch in v):
raise PydanticCustomError(
"not_katakana", # エラー種別
"{value}は全角カタカナで入力してください", # 利用者向けの文言
{"value": v},
)
return v
なお、v2ではTypeErrorは検証エラーに変換されません。バリデータ内の書き間違いで起きたTypeErrorは、422ではなく例外のまま上がります。
field_validatorのmode4種を受け取る値の違いで使い分ける
modeは、Pydantic自身の型変換と検証に対して、バリデータをいつ動かすかの指定です。4種類の違いを表にまとめます。
| mode | 受け取る値 | 型の検証 |
|---|---|---|
| after(既定) | 型変換後の値 | 先に行われる |
| before | 生の入力 | 戻り値に対し後で行う |
| plain | 生の入力 | 行わない |
| wrap | 生の入力とhandler | handlerを呼んだときに行う |
afterとbeforeの違い:型変換の前後で受け取る値が変わる
afterは型変換が済んだ後に動くので、intのフィールドなら必ずintが届きます。beforeは変換の前に動き、受け取る値は何が来るか分かりません。その代わり、型変換では受け付けない形式を、受け付ける形に直してから渡せます。
from typing import Any
from pydantic import BaseModel, field_validator
class SearchQuery(BaseModel):
tags: list[str]
limit: int
@field_validator("tags", mode="before")
@classmethod
def split_csv(cls, v: Any) -> Any:
# "python,fastapi" のようなカンマ区切りもリストとして受け付ける
if isinstance(v, str):
return [t.strip() for t in v.split(",") if t.strip()]
return v # それ以外はそのまま型の検証に回す
@field_validator("limit") # mode="after"
@classmethod
def cap_limit(cls, v: int) -> int:
return min(v, 100) # ここでは v は int に変換済み
print(SearchQuery(tags="python, fastapi", limit="500"))
# tags=['python', 'fastapi'] limit=100
beforeでは、想定外の型が来たときに例外を出さず、そのまま返すのが定石です。後に続く型の検証が、正しいエラー種別でエラーを出してくれます。
plainとwrapで型の検証を置き換える・包むときの実装例
plainは型の検証そのものを置き換えます。バリデータが返した値は、型ヒントと合っていなくてもそのまま入るという動作です。「1,280円」のような表記を整数にしたいなど、型変換の規則を丸ごと自前にしたい場合に限って使います。
from typing import Any
from pydantic import BaseModel, ValidationError, ValidatorFunctionWrapHandler, field_validator
class PriceRow(BaseModel):
price: int
note: str
# plain:Pydantic の int 検証を通さないので、型は自分で保証する
@field_validator("price", mode="plain", json_schema_input_type=str | int)
@classmethod
def parse_yen(cls, v: Any) -> int:
if isinstance(v, str):
v = v.replace(",", "").removesuffix("円")
return int(v) # 数字でなければ ValueError になり検証エラー
# wrap:通常の検証を handler で呼び、失敗したときだけ救済する
@field_validator("note", mode="wrap")
@classmethod
def fallback_empty(cls, v: Any, handler: ValidatorFunctionWrapHandler) -> str:
try:
return handler(v)
except ValidationError:
return "" # 文字列にできない値は空文字として扱う
json_schema_input_typeは、plain・before・wrapで入力の型が型ヒントと異なるときに、JSON Schemaへ出す型を指定する引数です。afterで指定するとPydanticUserErrorになります。wrapは前処理と後処理を1つの関数に書ける反面、読みにくくなるため、ログ出力やエラー時の救済のようにhandlerの前後両方に処理が要る場面に絞ります。
複数のバリデータが動く順番とAnnotatedのAfterValidatorとの関係
同じ処理はAnnotated[str, AfterValidator(関数)]の形でも書けます。公式の解説では、Annotatedの中のバリデータが動く順番は「beforeとwrapが右から左、続いてafterが左から右」です。デコレータで書いたバリデータは内部でこの形に変換され、Annotatedに書いた分の後ろ(右側)に足されます。
そのため、デコレータのafterはAnnotatedのafterより後に動き、デコレータのbeforeはAnnotatedのbeforeより先に動きます。順番に依存する処理を2つに分けると不具合のもとになるので、1つのフィールドに対するafterは1つの関数にまとめるのが無難です。複数のモデルで同じ規則を使い回すならAnnotated、1つのモデルの複数フィールドに同じ規則を当てるならデコレータ、という分け方が公式の勧める使い分けです。
他のフィールドを参照するValidationInfoとinfo.dataの使い方
バリデータの引数にinfo: ValidationInfoを足すと、検証済みの他のフィールドや、呼び出し側から渡した値を参照できます。
info.dataで先に定義したフィールドを参照する書き方と定義順の罠
from datetime import date
from pydantic import BaseModel, ValidationInfo, field_validator
class Booking(BaseModel):
check_in: date
check_out: date # check_in より下に定義する
@field_validator("check_out")
@classmethod
def after_check_in(cls, v: date, info: ValidationInfo) -> date:
start = info.data.get("check_in") # 検証に失敗していればキーが無い
if start is not None and v <= start:
raise ValueError("チェックアウトはチェックイン翌日以降にしてください")
return v
info.dataに入るのは、定義順で前にあり、検証を通ったフィールドだけです。check_inの形式が誤っていると辞書にキーが無いため、info.data["check_in"]と書くとKeyErrorで落ちます。.get()で受けるのが安全です。
この書き方の利点は、エラーの位置(loc)がcheck_outになることです。同じ判定をmodel_validatorで書くと位置がモデル全体になり、画面のどの入力欄にエラーを出すかを利用者側で判断できません。
‘*’とcheck_fieldsで全フィールドや継承先へ適用する方法
フィールド名に"*"を渡すと、サブクラスで足したものも含む全フィールドに適用されます。全文字列の前後の空白を落とす、といった共通処理に向きます。
from typing import Any
from pydantic import BaseModel, field_validator
class StrippedModel(BaseModel):
@field_validator("*", mode="before")
@classmethod
def strip_all(cls, v: Any) -> Any:
return v.strip() if isinstance(v, str) else v
class BaseAudit(BaseModel):
# このクラスに無い updated_by を、継承先に任せて検証する
@field_validator("updated_by", check_fields=False)
@classmethod
def not_system(cls, v: str) -> str:
if v == "system":
raise ValueError("system は指定できません")
return v
class Invoice(StrippedModel, BaseAudit):
title: str
updated_by: str
既定では、存在しないフィールド名を指定するとクラス定義の時点でエラーになります。基底クラスに書いて継承先のフィールドを検証したいときだけcheck_fields=Falseを付けます。打ち間違いの検出が効かなくなるため、乱用は禁物です。
contextでリクエストごとの設定値をバリデータへ渡す方法
呼び出しのたびに変わる値(テナントごとの上限、禁止語の一覧など)は、model_validate()のcontext引数で渡し、info.contextで受け取ります。
from pydantic import BaseModel, ValidationInfo, field_validator
class Upload(BaseModel):
size_mb: int
@field_validator("size_mb")
@classmethod
def within_plan(cls, v: int, info: ValidationInfo) -> int:
limit = (info.context or {}).get("max_mb", 10)
if v > limit:
raise ValueError(f"このプランの上限は{limit}MBです")
return v
Upload.model_validate({"size_mb": 30}, context={"max_mb": 50}) # 通る
公式の解説のとおり、Upload(size_mb=30)のようにコンストラクタで作る場合はcontextを渡せません。contextを使うモデルはmodel_validate()経由で作る、と決めておきます。
model_validatorのbefore・after・wrapの引数の違いと実装例
model_validatorはmodeの指定が必須で、引数を省くとエラーになります。modeごとにメソッドの形が異なります。
| mode | メソッドの形 | 返す値 |
|---|---|---|
| after | インスタンスメソッド(self) | self |
| before | classmethod(cls, data) | 検証に回す入力 |
| wrap | classmethod+handler | インスタンス |
afterで複数フィールドの整合性を検証しselfを返す書き方
from typing_extensions import Self
from pydantic import BaseModel, model_validator
class PasswordChange(BaseModel):
current: str
new: str
new_confirm: str
@model_validator(mode="after")
def check_rules(self) -> Self:
if self.new != self.new_confirm:
raise ValueError("新しいパスワードが確認欄と一致しません")
if self.new == self.current:
raise ValueError("現在と同じパスワードは使えません")
return self # 必ず self を返す
afterは全フィールドの検証が済んだ後に1回だけ動くため、3つ以上のフィールドを見比べる規則はここに置きます。v2.12からは、afterを@classmethodで書く形式は非推奨で、警告の対象です。上の例のとおりselfを受け取るインスタンスメソッドで書きます。BaseModel自体の基本はPydanticの読み方・BaseModelの使い方とV1との違いで扱っています。
beforeで入力辞書を組み替えるときにdict以外の入力へ備える方法
from typing import Any
from pydantic import BaseModel, model_validator
class Address(BaseModel):
pref: str
city: str
@model_validator(mode="before")
@classmethod
def accept_legacy(cls, data: Any) -> Any:
# 旧形式 {"address": "東京都 千代田区"} を新形式へ組み替える
if isinstance(data, dict) and "address" in data:
pref, _, city = data["address"].partition(" ")
data = {"pref": pref, "city": city}
return data
beforeのdataは辞書とは限りません。from_attributesを有効にしてORMのオブジェクトを渡した場合などは、任意のオブジェクトが届きます。isinstance(data, dict)で確かめてから触り、それ以外は手を加えずに返します。ORMのオブジェクトをmodel_validateで変換するときの遅延ロードの扱いは、Pydantic model_dumpの使い方で説明しています。
wrapで検証失敗をログに残す書き方とvalidate_assignmentの注意
import logging
from typing import Any
from typing_extensions import Self
from pydantic import BaseModel, ModelWrapValidatorHandler, ValidationError, model_validator
logger = logging.getLogger(__name__)
class WebhookEvent(BaseModel):
event_id: str
kind: str
@model_validator(mode="wrap")
@classmethod
def log_failure(cls, data: Any, handler: ModelWrapValidatorHandler[Self]) -> Self:
try:
return handler(data)
except ValidationError as e:
# 入力値そのものは個人情報を含みうるので、件数と種別だけ残す
logger.warning("webhook rejected: %s", [err["type"] for err in e.errors()])
raise
外部から届くデータの検証失敗を、例外として上げつつ記録したいときにwrapが役立ちます。ログに入力値を丸ごと出すと、改行を含む値でログが偽装されたり、個人情報が残ったりするため、種別だけを出す形にしています。
ConfigDictでvalidate_assignment=Trueにすると、属性への代入時にもバリデータが動きます。公式の移行ガイドによると、このときmodel_validatorは辞書ではなくモデルのインスタンスを受け取ることがあります。beforeで辞書を前提にした処理を守るために入れているのが、上のisinstance判定です。設定の詳細はConfigDictの設定一覧にまとめています。
v1の@validator・@root_validatorからv2へ書き換える手順
v2.14.3のdeprecated配下のclass_validators.pyを見ると、@validatorはDeprecationWarningを出したうえで動きます。pre=Trueはmode="before"に読み替えられる作りです。
pre・always・each_item・valuesを置き換える対応表
| v1の書き方 | v2の書き方 |
|---|---|
| @validator(“x”) | @field_validator(“x”) |
| pre=True | mode=”before” |
| always=True | validate_default=True |
| each_item=True | list[Annotated[…]] |
| 引数values | info.data |
| 引数field | info.field_name |
| 引数config | info.config |
| allow_reuse=True | 削除する |
| @root_validator(pre=True) | mode=”before” |
| root_validator(pre無し) | mode=”after” |
つまずきやすいのはeach_itemです。v2のfield_validatorにはこの引数が無く、リストの要素ごとに検証したいときは型の側に書きます。
# v1:@validator("scores", each_item=True)
# v2:要素の型に AfterValidator を付ける
from typing import Annotated
from pydantic import AfterValidator, BaseModel
def within_100(v: int) -> int:
if not 0 <= v <= 100:
raise ValueError("0〜100で指定してください")
return v
class Exam(BaseModel):
scores: list[Annotated[int, AfterValidator(within_100)]]
エラーの位置はscores.1のように要素の番号まで示されます。範囲の判定だけならlist[Annotated[int, Field(ge=0, le=100)]]と書けば、JSON Schemaにも出力されます。
root_validatorの起動時エラーと既定値が検証されない件
v1のまま@root_validatorを引数なしで書くと、2.14.3ではクラス定義の時点でPydanticUserErrorになります。pre=False(既定)ではskip_on_failure=Trueの指定が必須になったためです。警告ではなく起動できなくなるので、移行で最初に手を付ける箇所です。
もう1つは既定値です。v2では既定値は検証されず、カスタムのバリデータも既定値には動きません。v1でalways=Trueを付けていたフィールドは、Field(validate_default=True)を足さないと、既定値のときだけ検証が抜けます。移行後のテストは-W error::DeprecationWarningを付けて走らせ、残った@validatorを洗い出します。
Field制約とfield_validator・model_validatorの選び方
3つの手段は、表せる条件の範囲と、JSON Schemaに出るかどうかで選びます。
| 条件の例 | 書く場所 | Schema |
|---|---|---|
| 1以上100以下 | Field(ge=1, le=100) | 出る |
| 全角カタカナのみ | field_validator | 出ない |
| 終了日が開始日より後 | field_validator+info.data | 出ない |
| 3項目以上の組み合わせ | model_validator(after) | 出ない |
| 旧形式の入力の組み替え | model_validator(before) | 出ない |
優先する順はFieldの制約、field_validator、model_validatorです。Fieldの引数で書ける条件は仕様書にも載るため、フロントエンドと同じ規則を共有できます。制約引数の一覧はPydantic Fieldの使い方と引数一覧にまとめました。
FastAPIの422応答にバリデータのメッセージが載る形と直し方
FastAPIのリクエストボディに上のBookingを使うと、バリデータで送出したエラーは422応答のdetailに入ります(抜粋)。
{
"detail": [
{
"type": "value_error",
"loc": ["body", "check_out"],
"msg": "Value error, チェックアウトはチェックイン翌日以降にしてください",
"input": "2026-12-01"
}
]
}
ValueErrorの文言にはValue error, が前に付きます。画面にそのまま出す場合の対応は、PydanticCustomErrorで送出して接頭辞を外すか、typeで分岐して文言を差し替える方法です。422の読み方はFastAPIでPOSTを受け取る方法と422の直し方で扱っています。
入力検証は、どの層で何を止めるかの設計が先にあり、バリデータはその実装手段です。許可リストの考え方は入力バリデーションの許可リスト設計を参照してください。既存APIの入力チェックをPydanticのモデルへ集約し直す改修は、API開発・システム連携の受託開発でご相談を受け付けています。
よくある質問
field_validatorとmodel_validatorについて出てくることの多い質問に、2026年11月時点の版(pydantic 2.14.3)で答えます。
field_validatorに書いた条件はOpenAPIの仕様書に出ますか?
出ません。バリデータの中身はPythonの処理なので、JSON Schemaに変換できないためです。before・plain・wrapでjson_schema_input_typeを指定すると入力の型だけは変えられますが、条件そのものは載りません。仕様書に載せたい条件はFieldの制約で表し、表せない分だけをdescriptionに文章で書き添えます。
エラーメッセージを日本語にするにはどうすればよいですか?
ValueErrorに日本語の文言を渡せば、そのままメッセージになります。ただし先頭に付く接頭辞はValue error, です。接頭辞なしで、種別も独自にしたいならPydanticCustomError("種別名", "文言", 値の辞書)で送出します。Pydantic組み込みのエラー(int_parsingなど)の文言を変えたい場合は、ValidationError.errors()のtypeを見て差し替える処理を別に書きます。
v1の@validatorは2.14でもそのまま動きますか?
動きますが、使うたびにDeprecationWarningが出ます。また、fieldとconfigを受け取るシグネチャは使えなくなっており、該当する関数はエラーになります。@root_validatorはskip_on_failure=Trueが無いと起動時にエラーです。新しく書くコードは最初からfield_validatorとmodel_validatorにそろえます。
バリデータの中でDBに問い合わせて重複を確かめてもよいですか?
勧めません。モデルを作るたびにDBへの接続が要るため、テストや一括の読み込みが重くなり、レスポンスモデルの生成でも問い合わせが走ります。バリデータは入力の形と値の範囲を確かめるところまでに留め、メールアドレスの重複のような業務上の判定は、サービス層でまとめて行う方が責務の線引きが明快です。
設定ファイルの値にもfield_validatorは使えますか?
使えます。pydantic-settingsのBaseSettingsはBaseModelを土台にしているため、同じデコレータで環境変数の値を検証できます。カンマ区切りの環境変数をリストにするbeforeバリデータは定番の使い方です。設定の読み込み方はpydantic-settingsの使い方で解説しています。
関連記事
- Pydanticとは?読み方・BaseModelの使い方とV1との違い【2.14対応】:バリデータの土台になるBaseModelの入門です。
- Pydantic Fieldの使い方:default_factory・alias・制約の引数一覧とOpenAPIへの反映【2.14対応】:バリデータより先に検討するFieldの制約引数の一覧です。
- Pydantic v2のConfigDict設定一覧|model_configの書き方とv1からの移行【2.14対応】:validate_assignmentなどモデル全体の設定です。
- FastAPIでPOSTを受け取る方法|JSON・Form・ファイル・Requestの使い分けと422の直し方:バリデータのエラーが返る422の読み方です。
- 入力バリデーションとは?防げる攻撃の範囲と許可リスト設計を実装目線で解説:どの層で何を検証するかの設計です。