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の書き方もかなり変わっています。
| 項目 | V1 | V2 |
|---|---|---|
| バリデータ | @validator | @field_validator |
| ルートバリデータ | @root_validator | @model_validator |
| 設定クラス | class Config: | model_config = ConfigDict() |
| コアエンジン | Pure Python | Rust (pydantic-core) |
| バリデーション速度 | 1x | 5〜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_configのalias_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_validator、class 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が最も適しています。

