Pydantic V2のバリデーション設計—よくあるミスと正しい書き方

Pydantic V2のバリデーション設計—よくあるミスと正しい書き方 | mohablog

FastAPIやDjangoNinjaなど、最近のPython WebフレームワークではリクエストのバリデーションにPydanticを使うのがほぼ標準になっています。自分もFastAPIでAPIを作るときに「なんとなくBaseModelを継承すればいい」くらいの認識で使い始めたんですが、V2になってからバリデータの書き方がかなり変わっていて、公式ドキュメントを読み直す羽目になりました。

この記事では、Pydantic V2(2.7)のバリデーション機能を体系的に整理します。基本的なモデル定義からfield_validator・model_validatorの使い分け、ありがちなNGパターンまで、実際のコード付きで解説していきます。

目次

Pydanticとは?データバリデーションが必要な理由

PydanticはPythonのデータバリデーション・シリアライゼーションライブラリです。型アノテーションを使ってデータ構造を定義すると、入力値の検証や型変換を自動で行ってくれます。

Pydantic V2で何が変わったのか

V2はRustベースのコアエンジン(pydantic-core)に書き直されたメジャーアップデートで、バリデーション速度が5〜50倍高速化されました。APIの書き方もかなり変わっています。

項目V1V2
バリデータ@validator@field_validator
ルートバリデータ@root_validator@model_validator
設定クラスclass Config:model_config = ConfigDict()
コアエンジンPure PythonRust (pydantic-core)
バリデーション速度1x5〜50x

バリデーションなしのコードが抱えるリスク

まず、バリデーションを使わない場合にどうなるか見てみます。

# NGパターン: dictで受け取ってif文で頑張る
def create_user(data: dict) -> dict:
    if "name" not in data:
        raise ValueError("nameは必須です")
    if not isinstance(data["name"], str):
        raise ValueError("nameは文字列にしてください")
    if len(data["name"]) > 50:
        raise ValueError("nameは50文字以内にしてください")
    if "age" in data and not isinstance(data["age"], int):
        raise ValueError("ageは整数にしてください")
    # ...延々と続く
    return data

これはフィールドが増えるたびにif文が際限なく膨らんでいきます。Pydanticを使えば、こうなります。

from pydantic import BaseModel, Field

class User(BaseModel):
    name: str = Field(max_length=50)
    age: int | None = None

user = User(name="田中太郎", age=28)
print(user.model_dump())
{'name': '田中太郎', 'age': 28}

宣言的に書けるので、コードの見通しが格段に良くなります。

BaseModelの基本—フィールド定義と型変換

基本的なモデル定義

PydanticのモデルはBaseModelを継承して定義します。型アノテーションがそのままバリデーションルールになるのがポイントです。

from pydantic import BaseModel
from datetime import datetime

class Article(BaseModel):
    title: str
    body: str
    published: bool = False
    created_at: datetime | None = None

# 文字列で渡してもdatetimeに自動変換される
article = Article(
    title="Pydantic入門",
    body="本文です",
    created_at="2026-04-13T10:00:00"
)
print(article.created_at)
print(type(article.created_at))
2026-04-13 10:00:00
<class 'datetime.datetime'>

文字列の"2026-04-13T10:00:00"が自動的にdatetimeオブジェクトに変換されています。この「型変換(coercion)」がPydanticの強力なポイントです。

Fieldオプションで制約を加える

Fieldを使うと、最小値・最大値・文字数制限などの制約を宣言的に追加できます。

from pydantic import BaseModel, Field

class Product(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    price: int = Field(gt=0, description="税込価格(円)")
    quantity: int = Field(ge=0, le=9999, default=0)

# 正常なデータ
product = Product(name="Pythonの教科書", price=2980)
print(product)

# バリデーションエラー
try:
    Product(name="", price=-100)
except Exception as e:
    print(e)
name='Pythonの教科書' price=2980 quantity=0
2 validation errors for Product
name
  String should have at least 1 character [type=string_too_short, ...]
price
  Input should be greater than 0 [type=greater_than, ...]

エラーメッセージもフィールドごとに自動生成されるので、APIのレスポンスにそのまま使えて便利です。

field_validatorで入力を検証する

基本的なバリデータの書き方

Fieldの組み込みオプションだけでは表現できないカスタムバリデーションには@field_validatorを使います。

from pydantic import BaseModel, field_validator

class SignupForm(BaseModel):
    email: str
    password: str

    @field_validator("email")
    @classmethod
    def email_must_contain_at(cls, v: str) -> str:
        if "@" not in v:
            raise ValueError("有効なメールアドレスを入力してください")
        return v

    @field_validator("password")
    @classmethod
    def password_strength(cls, v: str) -> str:
        if len(v) < 8:
            raise ValueError("パスワードは8文字以上にしてください")
        if v.isalpha() or v.isdigit():
            raise ValueError("英字と数字を両方含めてください")
        return v

# テスト
try:
    SignupForm(email="invalid", password="abc")
except Exception as e:
    print(e)
2 validation errors for SignupForm
email
  Value error, 有効なメールアドレスを入力してください [type=value_error, ...]
password
  Value error, パスワードは8文字以上にしてください [type=value_error, ...]

V2では@classmethodデコレータを一緒に付けるのが正式な書き方です。V1の@validatorとは書き方が違うので注意してください。

before/afterモードの使い分け

field_validatorにはmodeパラメータがあり、バリデーションのタイミングを制御できます。

モード実行タイミング用途
after(デフォルト)型変換の後変換済みの値をチェック
before型変換の前入力値の前処理・正規化
wrap型変換の前後変換処理自体をカスタマイズ
from pydantic import BaseModel, field_validator

class Tag(BaseModel):
    name: str

    @field_validator("name", mode="before")
    @classmethod
    def normalize_name(cls, v):
        """入力値を小文字に正規化してからバリデーションへ"""
        if isinstance(v, str):
            return v.strip().lower()
        return v

tag = Tag(name="  Python  ")
print(tag.name)
python

mode="before"にすると型変換より先に実行されるので、空白の除去や大文字小文字の正規化といった前処理に向いています。

model_validatorでフィールド間の整合性を保つ

複数フィールドの相関チェック

「開始日は終了日より前でなければならない」のような、複数フィールドにまたがるバリデーションには@model_validatorを使います。

from datetime import date
from pydantic import BaseModel, model_validator

class DateRange(BaseModel):
    start_date: date
    end_date: date

    @model_validator(mode="after")
    def check_date_order(self):
        if self.start_date >= self.end_date:
            raise ValueError("start_dateはend_dateより前の日付にしてください")
        return self

# 正常
r = DateRange(start_date="2026-04-01", end_date="2026-04-30")
print(r)

# エラー
try:
    DateRange(start_date="2026-04-30", end_date="2026-04-01")
except Exception as e:
    print(e)
start_date=datetime.date(2026, 4, 1) end_date=datetime.date(2026, 4, 30)
1 validation error for DateRange
  Value error, start_dateはend_dateより前の日付にしてください [type=value_error, ...]

beforeモードでの前処理

mode="before"のmodel_validatorでは、まだモデルが構築される前のdictを受け取れます。フィールド名の正規化やデフォルト値の動的な設定に使えます。

from pydantic import BaseModel, model_validator

class ApiPayload(BaseModel):
    user_name: str
    email: str

    @model_validator(mode="before")
    @classmethod
    def normalize_keys(cls, data):
        """キャメルケースのキーをスネークケースに変換"""
        if isinstance(data, dict) and "userName" in data:
            data["user_name"] = data.pop("userName")
        return data

payload = ApiPayload(**{"userName": "tanaka", "email": "t@example.com"})
print(payload.user_name)
tanaka

外部APIから受け取るJSONのキー名がキャメルケースの場合に、このパターンが役立ちます。ただし、この用途ならmodel_configalias_generatorを使うほうがスマートな場合もあります。

NGパターンから学ぶ---ありがちなバリデーションミス

classmethodデコレータの付け忘れ

V2の@field_validatorでは@classmethodを付けるのが正式な書き方です。付け忘れると警告が出ます。

# NG: @classmethodが抜けている
class BadModel(BaseModel):
    name: str

    @field_validator("name")
    def check_name(cls, v):  # 警告が出る
        return v

# OK: @classmethodを付ける
class GoodModel(BaseModel):
    name: str

    @field_validator("name")
    @classmethod
    def check_name(cls, v: str) -> str:
        return v

バリデーションエラーを握りつぶす

もう一つよく見るのが、try-exceptでバリデーションエラーを雑にキャッチしてしまうパターンです。

# NG: エラーを握りつぶしてデフォルト値を返す
def parse_user(data: dict):
    try:
        return User(**data)
    except Exception:
        return User(name="unknown", age=0)  # 不正なデータが静かに通る
# OK: ValidationErrorを適切にハンドリングする
from pydantic import ValidationError

def parse_user(data: dict):
    try:
        return User(**data)
    except ValidationError as e:
        logger.warning("バリデーション失敗: %s", e.error_count())
        raise

PydanticのValidationErrorはエラーの詳細(どのフィールドがどう不正か)を構造化して持っています。これを握りつぶすと、本番環境でデータ不整合の原因を追えなくなります。

FastAPIとの連携---リクエストバリデーション

リクエストボディの自動バリデーション

FastAPIはPydanticモデルをリクエストボディの型として受け取るだけで、自動的にバリデーションとエラーレスポンスの生成を行ってくれます。

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class CreateArticleRequest(BaseModel):
    title: str = Field(min_length=1, max_length=100)
    body: str = Field(min_length=1)
    tags: list[str] = Field(default_factory=list, max_length=5)

@app.post("/articles")
async def create_article(req: CreateArticleRequest):
    return {"message": f"記事 '{req.title}' を作成しました"}

不正なリクエストが来ると、FastAPIが自動的に422エラーとともにPydanticのバリデーションエラー詳細をJSON形式で返します。バリデーションロジックを自分で書く必要がないので、コードがすっきりします。関連記事としてFastAPIで作るREST API入門:Pythonで高速なAPI開発を始めようもあわせて参考にしてみてください。

レスポンスモデルの定義

レスポンスにもPydanticモデルを使うことで、APIの出力を型安全に制御できます。

from pydantic import ConfigDict

class ArticleResponse(BaseModel):
    id: int
    title: str
    tags: list[str]

    model_config = ConfigDict(from_attributes=True)

@app.post("/articles", response_model=ArticleResponse)
async def create_article(req: CreateArticleRequest):
    article = save_to_db(req)  # DB保存
    return article  # ORMオブジェクトでもfrom_attributes=Trueで自動変換

from_attributes=True(旧orm_mode=True)を設定すると、SQLAlchemyなどのORMオブジェクトから直接Pydanticモデルに変換できます。型ヒントの活用方法についてはPythonの型ヒントを使いこなす!mypyでコード品質を大幅向上させる方法も参考になると思います。

まとめ

  • Pydantic V2はRustベースのコアで5〜50倍高速化された
  • BaseModel + Fieldで宣言的にバリデーションルールを定義できる
  • field_validatorで単一フィールドのカスタムバリデーション、model_validatorで複数フィールドの相関チェックを行う
  • field_validatorのbefore/afterモードを使い分けることで、入力の前処理と検証を分離できる
  • バリデーションエラーは握りつぶさず、ValidationErrorの構造化情報を活用する
  • FastAPIと組み合わせると、リクエスト/レスポンスのバリデーションが自動化される

よくある質問(FAQ)

Q. Pydantic V1のコードをV2にそのまま移行できますか?

完全な互換性はありません。特に@validatorから@field_validatorclass Configからmodel_config = ConfigDict()への書き換えが必要です。公式が提供しているbump-pydanticというCLIツールを使うと、基本的な書き換えを自動で行ってくれます。

Q. field_validatorとmodel_validatorはどう使い分ければいいですか?

単一フィールドの値チェックや変換にはfield_validator、複数フィールドにまたがる整合性チェック(例: 開始日と終了日の前後関係)にはmodel_validatorを使います。迷ったら「そのバリデーションは1つのフィールドだけで完結するか?」を基準に判断するといいです。

Q. PydanticとdataclassesやAttrsの違いは何ですか?

Pythonの標準dataclassesはデータ構造の定義には便利ですが、バリデーション機能はありません。attrsはバリデーション機能がありますが、Pydanticほど充実してはいません。PydanticはJSON Schema生成やFastAPIとのネイティブ連携など、Web開発向けの機能が揃っている点が最大の強みです。外部APIやユーザー入力など、信頼できないデータを扱う場面ではPydanticが最も適しています。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次