404 Not Found

404 Not Found


nginx

Request Body and Data Validation — In-Depth Practical Guide to Pydantic V2

データバリデーションは空港のセキュリティチェックのようなものです。すべての乗客 (リクエスト)は, 身分確認 (型チェック), 手荷物検査 (制約バリデーション), 申告内容の確認 (カスタムバリデータ)を経てからでないと飛行機 (ビジネスロジック)に搭乗できません。

1. 学ぶ内容


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) 完全なライフサイクル

100%
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 移行比較

100%
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_validatormodel_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()examplesjson_schema_extra の違いは何ですか?
A examples はフィールドのサンプル値のリストで, OpenAPI examples にマッピングされます。json_schema_extra はモデルレベルの追加スキーマプロパティです。
Q ネストされたモデルが深すぎると問題はありますか?
A 3レベルを超えるネストはバリデーションの遅延が増加し, デバッグが困難になります。構造のフラット化やコンポジットパターンによる分割をお勧めします。
Q Pydantic V2 のパフォーマンスは V1 と比べてどれくらい向上していますか?
A コアバリデーションは 5-50 倍高速 (Rust 実装), シリアライズは 2-10 倍高速です。数百万データを扱うシナリオで顕著な改善が見られます。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度 ⭐):PriceCreate モデルを作成し, product_id: int (> 0)と price: float (> 0)を含め, 無効な入力がバリデーションエラーをトリガーすることを確認してください。ヒント:BaseModel + Field(gt=0)
  2. 応用問題 (難易度 ⭐⭐):PriceCreate@field_validator を追加して currency フィールドが USD/EUR/GBP のみであることをバリデーションし, @model_validator を追加して min_price <= max_price を確認してください。ヒント:field_validator + model_validator(mode="after")
  3. チャレンジ問題 (難易度 ⭐⭐⭐):PriceTracker のための完全なネストモデルシステムを設計してください:ProductCreatePriceInfo (amount + currency)を含め, PriceInfo の currency は正規表現で制約し, ProductCreate の SKU は pattern でフォーマット制約してください。ヒント:Field(pattern=...) + ネストされた BaseModel

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%