---
title: "Pydantic field_validatorの使い方：mode4種の違いとmodel_validator・v1からの移行【2.14対応】"
url: "https://www.issoh.co.jp/tech/details/19775/"
published: 2026-11-14
updated: 2026-11-14
categories: ["開発"]
publisher: "株式会社一創"
---

# 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](https://pypi.org/project/pydantic/)では、2026年11月14日時点の最新が2.14.3（11月5日公開・Python 3.10以上）です。

### デコレータの引数とclassmethodを省略できる条件と戻り値の注意

[v2.14.3のfunctional\_validators.py](https://github.com/pydantic/pydantic/blob/v2.14.3/pydantic/functional%5Fvalidators.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の解説](https://pydantic.dev/docs/validation/latest/concepts/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との違い](https://www.issoh.co.jp/tech/details/4838/)で扱っています。

### 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の使い方](https://www.issoh.co.jp/tech/details/19777/)で説明しています。

### 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`にすると、属性への代入時にもバリデータが動きます。[公式の移行ガイド](https://pydantic.dev/docs/validation/latest/get-started/migration/)によると、このとき`model_validator`は辞書ではなくモデルのインスタンスを受け取ることがあります。beforeで辞書を前提にした処理を守るために入れているのが、上の`isinstance`判定です。設定の詳細は[ConfigDictの設定一覧](https://www.issoh.co.jp/tech/details/7583/)にまとめています。

## v1の@validator・@root\_validatorからv2へ書き換える手順

[v2.14.3のdeprecated配下のclass\_validators.py](https://github.com/pydantic/pydantic/blob/v2.14.3/pydantic/deprecated/class%5Fvalidators.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の使い方と引数一覧](https://www.issoh.co.jp/tech/details/19771/)にまとめました。

### 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の直し方](https://www.issoh.co.jp/tech/details/19737/)で扱っています。

入力検証は、どの層で何を止めるかの設計が先にあり、バリデータはその実装手段です。許可リストの考え方は[入力バリデーションの許可リスト設計](https://www.issoh.co.jp/tech/details/16364/)を参照してください。既存APIの入力チェックをPydanticのモデルへ集約し直す改修は、[API開発・システム連携の受託開発](https://www.issoh.co.jp/service/system/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の使い方](https://www.issoh.co.jp/tech/details/19769/)で解説しています。

## 関連記事

- [Pydanticとは？読み方・BaseModelの使い方とV1との違い【2.14対応】](https://www.issoh.co.jp/tech/details/4838/)：バリデータの土台になるBaseModelの入門です。
- [Pydantic Fieldの使い方：default\_factory・alias・制約の引数一覧とOpenAPIへの反映【2.14対応】](https://www.issoh.co.jp/tech/details/19771/)：バリデータより先に検討するFieldの制約引数の一覧です。
- [Pydantic v2のConfigDict設定一覧｜model\_configの書き方とv1からの移行【2.14対応】](https://www.issoh.co.jp/tech/details/7583/)：validate\_assignmentなどモデル全体の設定です。
- [FastAPIでPOSTを受け取る方法｜JSON・Form・ファイル・Requestの使い分けと422の直し方](https://www.issoh.co.jp/tech/details/19737/)：バリデータのエラーが返る422の読み方です。
- [入力バリデーションとは？防げる攻撃の範囲と許可リスト設計を実装目線で解説](https://www.issoh.co.jp/tech/details/16364/)：どの層で何を検証するかの設計です。

---

出典: [Pydantic field\_validatorの使い方：mode4種の違いとmodel\_validator・v1からの移行【2.14対応】](<https://www.issoh.co.jp/tech/details/19775/>)（株式会社一創）
