開発

Pydantic field_validatorの使い方:mode4種の違いとmodel_validator・v1からの移行【2.14対応】

Pydantic field_validatorの使い方:mode4種の違いとmodel_validator・v1からの移行【2.14対応】

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の使い方で解説しています。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.13 テックブログ 宝くじ公式サイト不正アクセス:4,436名の対象確認とパスワード再設定、変更された仕組み
  2. 2026.11.13 テックブログ KARTEの不正アクセス:全機能停止で利用企業がタグ・配信・データ連携を確かめる手順
  3. 2026.10.21 テックブログ マネックス証券の不正アクセス:メールアドレス最大約113万件と本人確認画像1,592件の漏えいのおそれ、口座開設停止とPHP脆弱性との関係
  4. 2026.11.14 テックブログ ヤマダデンキの不正アクセス:リフォーム管理システム最大350万件と、住所・見積を持つ営業記録の守り方
  5. 2026.11.11 テックブログ Microsoft 2026年11月の月例更新:Outlookの悪用済みCVE-2026-124194と修正版の確認方法

RELATED POSTS 関連記事

目次