Request Body and Data Validation — In-Depth Practical Guide to Pydantic V2
データバリデーションは空港のセキュリティチェックのようなものです。すべての乗客 (リクエスト)は, 身分確認 (型チェック), 手荷物検査 (制約バリデーション), 申告内容の確認 (カスタムバリデータ)を経てからでないと飛行機 (ビジネスロジック)に搭乗できません。
1. 学ぶ内容
- Pydantic V2
BaseModel:field_validator/model_validatorが V1 の@validatorに代わる Field()高度な制約:gt/lt/pattern/examplesと JSON Schema 生成- ネストされたモデルとモデルの組み合わせ:
Optional,Union,Literalの実践的活用 model_config:ConfigDictが V1 のclass Configとfrom_attributes=TrueORM モードに代わる- Alice シナリオ:PriceTracker 商品価格送信のための完全な Pydantic モデル設計
2. Alice のリアルストーリー
(1) ペインポイント:価格データの送信とバリデーションにエラーが多発
Alice の PriceTracker はサプライヤーから送信される価格データを受け取ります。価格は 0 より大きく, 通貨は ISO 4217 の3文字コードで指定し, 商品名を空にできないという要件があります。しかし, Bob のフロントエンドは時折負の価格や無効な通貨を送信してきます。Flask で書かれたカスタムバリデーションコードは 10 箇所に散在しており, 「価格が 0」というエッジケースを見落としているため, データベースに価格 0 のダーティデータが存在しています。
(2) Pydantic V2 における宣言的バリデーションの解決策
Pydantic V2 は命令的バリデーションを宣言的モデルに置き換えます。すべての制約はモデル内で定義され, 一度定義すればグローバルに適用されます。
PYTHON
from pydantic import BaseModel, Field, field_validator
class PriceCreate(BaseModel):
product_id: int = Field(gt=0)
price: float = Field(gt=0, description="Price in USD")
currency: str = Field(pattern=r"^[A-Z]{3}$", examples=["USD", "EUR"])
@field_validator("currency")
@classmethod
def validate_currency(cls, v: str) -> str:
if v not in {"USD", "EUR", "GBP", "JPY", "CNY"}:
raise ValueError(f"Unsupported currency: {v}")
return v
(3) 成果
価格バリデーションコードは 10 の独立したロジックブロックから1つのモデル定義に統合されました。0 や負の価格は自動的にフィルタリングされ, 通貨バリデーションは正規表現とカスタムバリデータの2層アプローチで保証され, データベースのダーティデータが排除されました。
3. Pydantic V2 データフロー
(1) 完全なライフサイクル
flowchart TD
A[JSON Request Body] --> B[model_validate]
B --> C[Type Coercion]
C --> D[field_validator]
D --> E[model_validator]
E --> F[Valid Model Instance]
F --> G[model_dump]
G --> H[JSON Response]
F --> I[model_dump_json]
I --> J[JSON String]
| フェーズ | メソッド | 説明 |
|---|---|---|
| 入力パース | model_validate(data) |
dict/JSON からパース・バリデーション |
| 型変換 | 自動 | "42" → 42, "9.99" → 9.99 |
| フィールドバリデーション | @field_validator |
単一フィールドのカスタムバリデーション |
| モデルバリデーション | @model_validator |
クロスフィールドの合同バリデーション |
| 出力シリアライズ | model_dump() / model_dump_json() |
モデルを dict/JSON に変換 |
(1) ▶ サンプル:Pydantic V2 Base Model
PYTHON
from pydantic import BaseModel, Field
class ProductCreate(BaseModel):
name: str = Field(min_length=1, max_length=200)
category: str = Field(max_length=100)
base_price: float = Field(gt=0, description="Base price in USD")
# パースとバリデーション
data = {"name": "Widget", "category": "electronics", "base_price": 29.99}
product = ProductCreate.model_validate(data)
print(product.model_dump())
出力:
TEXT
{'name': 'Widget', 'category': 'electronics', 'base_price': 29.99}
4. field_validator と model_validator
(1) V1 → V2 移行比較
flowchart LR
V1[Pydantic V1] --> Migrate[V1 → V2 移行]
Migrate --> V2[Pydantic V2]
V1 --- A["@validator('field')"]
V2 --- B["@field_validator('field')"]
V1 --- C["@root_validator"]
V2 --- D["@model_validator(mode='after')"]
V1 --- E["class Config:"]
V2 --- F["model_config = ConfigDict(...)"]
V1 --- G[".dict()"]
V2 --- H[".model_dump()"]
| V1 構文 | V2 構文 | 説明 |
|---|---|---|
@validator("field") |
@field_validator("field") |
フィールドバリデータ |
@root_validator |
@model_validator(mode="after") |
モデルレベルバリデーション |
class Config: orm_mode = True |
model_config = ConfigDict(from_attributes=True) |
ORM モデル |
.dict() |
.model_dump() |
dict へシリアライズ |
.json() |
.model_dump_json() |
JSON へシリアライズ |
(1) ▶ サンプル:field_validator — 単一フィールドバリデーション
PYTHON
from pydantic import BaseModel, Field, field_validator
class PriceCreate(BaseModel):
product_id: int = Field(gt=0)
price: float = Field(gt=0)
currency: str = Field(default="USD", max_length=3)
@field_validator("price")
@classmethod
def price_precision(cls, v: float) -> float:
# 小数点第2位に丸める
return round(v, 2)
@field_validator("currency")
@classmethod
def valid_currency(cls, v: str) -> str:
allowed = {"USD", "EUR", "GBP", "JPY", "CNY"}
if v not in allowed:
raise ValueError(f"Currency must be one of {allowed}")
return v.upper()
# バリデーションのテスト
p = PriceCreate(product_id=1, price=9.999, currency="usd")
print(p.model_dump())
出力:
TEXT
{'product_id': 1, 'price': 10.0, 'currency': 'USD'}
(2) ▶ サンプル:model_validator クロスフィールドバリデーション
PYTHON
from pydantic import BaseModel, Field, model_validator
class PriceRangeQuery(BaseModel):
min_price: float = Field(ge=0, description="最低価格 (USD)")
max_price: float = Field(ge=0, description="最高価格 (USD)")
@model_validator(mode="after")
def check_range(self):
if self.min_price > self.max_price:
raise ValueError("min_price must be <= max_price")
return self
# 有効
valid = PriceRangeQuery(min_price=10, max_price=100)
print(valid.model_dump())
# 無効 - バリデーションエラーが発生
# PriceRangeQuery(min_price=100, max_price=10)
出力:
TEXT
{'min_price': 10.0, 'max_price': 100.0}
5. Field() 高度な制約と JSON Schema
(1) Field() パラメータクイックリファレンス
| パラメータ | 型 | 説明 | JSON Schema マッピング |
|---|---|---|---|
gt |
値 | より大きい | exclusiveMinimum |
ge |
値 | 以上 | minimum |
lt |
値 | より小さい | exclusiveMaximum |
le |
値 | 以下 | maximum |
min_length |
文字列 | 最小長 | minLength |
max_length |
文字列 | 最大長 | maxLength |
pattern |
文字列 | 正規表現 | pattern |
default |
Any | デフォルト | default |
examples |
List | サンプル値 | examples |
description |
文字列 | 説明 | description |
alias |
文字列 | フィールドエイリアス | エイリアスマッピング |
(1) ▶ サンプル:Field 制約と JSON Schema
PYTHON
from pydantic import BaseModel, Field
class ProductCreate(BaseModel):
name: str = Field(
min_length=1,
max_length=200,
description="商品表示名",
examples=["Wireless Mouse", "USB Cable"],
)
sku: str = Field(
pattern=r"^[A-Z]{2}-\d{4,6}$",
description="SKU コード: 2文字 + 4-6桁の数字",
examples=["EL-1234", "CB-567890"],
)
base_price: float = Field(
gt=0,
le=999999.99,
description="Base price in USD",
examples=[9.99, 49.99, 199.99],
)
# 生成された JSON Schema を確認
print(ProductCreate.model_json_schema())
出力:
TEXT
# 実行成功
6. ネストされたモデルとモデルの組み合わせ
(1) ネストされたモデル構造
(1) ▶ サンプル:PriceTracker のネストされたモデル
PYTHON
from pydantic import BaseModel, Field
from typing import Optional
class PriceInfo(BaseModel):
amount: float = Field(gt=0, description="価格金額 (USD)")
currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")
source: str = Field(max_length=100, description="価格ソース")
class ProductCreate(BaseModel):
name: str = Field(min_length=1, max_length=200)
category: str = Field(max_length=100)
current_price: PriceInfo # ネストされたモデル
original_price: Optional[PriceInfo] = None # 任意のネスト
# ネストされたバリデーション
data = {
"name": "Wireless Mouse",
"category": "electronics",
"current_price": {"amount": 29.99, "currency": "USD", "source": "Amazon"},
"original_price": {"amount": 49.99, "currency": "USD", "source": "Amazon"},
}
product = ProductCreate.model_validate(data)
print(product.model_dump())
出力:
TEXT
# 実行成功
(2) ▶ サンプル:Union と Literal
PYTHON
from pydantic import BaseModel, Field
from typing import Union, Literal
class SinglePrice(BaseModel):
type: Literal["single"] = "single"
amount: float = Field(gt=0)
class RangePrice(BaseModel):
type: Literal["range"] = "range"
min_amount: float = Field(gt=0)
max_amount: float = Field(gt=0)
class ProductPrice(BaseModel):
product_id: int = Field(gt=0)
pricing: Union[SinglePrice, RangePrice] # 判別可能なユニオン型
# FastAPI は "type" フィールドを使ってバリデーションするモデルを決定
data = {"product_id": 1, "pricing": {"type": "range", "min_amount": 10, "max_amount": 50}}
pp = ProductPrice.model_validate(data)
print(pp.model_dump())
出力:
TEXT
# 実行成功
(2) ConfigDict 設定
(3) ▶ サンプル:model_config と ORM パターン
PYTHON
from pydantic import BaseModel, ConfigDict
class ProductResponse(BaseModel):
model_config = ConfigDict(
from_attributes=True, # ORM モードを有効化 (SQLAlchemy オブジェクトから読み取り)
populate_by_name=True, # フィールド名とエイリアスの両方を許可
json_schema_extra={
"examples": [{"id": 1, "name": "Widget", "price": 9.99}]
},
)
id: int
name: str
price: float
# from_attributes=True で, オブジェクトの属性から作成可能
class FakeORMObject:
def __init__(self):
self.id = 1
self.name = "Widget"
self.price = 9.99
orm_obj = FakeORMObject()
response = ProductResponse.model_validate(orm_obj)
print(response.model_dump())
出力:
TEXT
{'id': 1, 'name': 'Widget', 'price': 9.99}
7. 総合サンプル
Pydantic V2 のフィールドバリデーションとクロスフィールドバリデーションは, ORM パターンと FastAPI のリクエストボディと組み合わせることで, 完全なデータ入力バリデーションワークフローを実現します。
PYTHON
from fastapi import FastAPI
from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict
class PriceCreate(BaseModel):
product_name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0, description="価格は0より大きくなければなりません")
currency: str = "USD"
@field_validator("currency")
@classmethod
def validate_currency(cls, v: str) -> str:
if v not in ("USD", "EUR", "GBP"):
raise ValueError("currency must be USD/EUR/GBP")
return v
class PriceRangeQuery(BaseModel):
min_price: float = Field(ge=0)
max_price: float = Field(ge=0)
@model_validator(mode="after")
def validate_range(self):
if self.min_price > self.max_price:
raise ValueError("min_price must <= max_price")
return self
app = FastAPI()
@app.post("/prices")
async def create_price(data: PriceCreate):
return data.model_dump()
出力:
TEXT
POST /prices {"product_name":"Widget","price":9.99} → {"product_name":"Widget","price":9.99,"currency":"USD"}
POST /prices {"product_name":"","price":-1} → 422 Validation Error
❓ よくある質問
Q
field_validator と model_validator はどう使い分けますか?A 単一フィールドのバリデーション (フォーマットチェックなど)には
field_validator を, クロスフィールドバリデーション (min_price <= max_price など)には model_validator を使用してください。Q V1 の @validator アノテーションはまだ使えますか?
A V2 は互換レイヤーを残していますが, 非推奨警告が発生します。新規プロジェクトでは @field_validator または @model_validator を使用し, 既存プロジェクトはできるだけ早く移行してください。
Q
from_attributes=True は何をしますか?A ORM オブジェクト (SQLAlchemy モデルインスタンスなど)から直接 Pydantic モデルを作成できるようにし, 辞書ではなくオブジェクトの属性を読み取ります。これは FastAPI + SQLAlchemy の重要な設定です。
Q
Field() の examples と json_schema_extra の違いは何ですか?A
examples はフィールドのサンプル値のリストで, OpenAPI examples にマッピングされます。json_schema_extra はモデルレベルの追加スキーマプロパティです。Q ネストされたモデルが深すぎると問題はありますか?
A 3レベルを超えるネストはバリデーションの遅延が増加し, デバッグが困難になります。構造のフラット化やコンポジットパターンによる分割をお勧めします。
Q Pydantic V2 のパフォーマンスは V1 と比べてどれくらい向上していますか?
A コアバリデーションは 5-50 倍高速 (Rust 実装), シリアライズは 2-10 倍高速です。数百万データを扱うシナリオで顕著な改善が見られます。
📖 まとめ
- Pydantic V2 は V1 の
@validatorと@root_validatorを@field_validatorと@model_validatorに置き換えました Field()は制約を自動的に JSON Schema にマッピングし, OpenAPI ドキュメントとリクエストデータバリデーションの両方を駆動します- ネストされたモデルは
Optional,Union,Literalの柔軟な組み合わせをサポートし,Literalは判別可能なユニオン型を実装します ConfigDict(from_attributes=True)で ORM モードを有効化し, SQLAlchemy オブジェクトから直接 Pydantic モデルを作成します- V1→V2 移行の核心:
.dict()→.model_dump(),class Config→model_config = ConfigDict(...)
📝 練習問題
- 基本問題 (難易度 ⭐):
PriceCreateモデルを作成し,product_id: int(> 0)とprice: float(> 0)を含め, 無効な入力がバリデーションエラーをトリガーすることを確認してください。ヒント:BaseModel+Field(gt=0) - 応用問題 (難易度 ⭐⭐):
PriceCreateに@field_validatorを追加してcurrencyフィールドが USD/EUR/GBP のみであることをバリデーションし,@model_validatorを追加してmin_price <= max_priceを確認してください。ヒント:field_validator+model_validator(mode="after") - チャレンジ問題 (難易度 ⭐⭐⭐):PriceTracker のための完全なネストモデルシステムを設計してください:
ProductCreateにPriceInfo(amount + currency)を含め,PriceInfoの currency は正規表現で制約し,ProductCreateの SKU はpatternでフォーマット制約してください。ヒント:Field(pattern=...)+ ネストされたBaseModel
---|



