Chuyển đến nội dung chính
友田 陽大
Pydantic & type-safe validation
Python
Pydantic
型安全
バリデーション
FastAPI
データモデリング
アーキテクチャ設計

Pydantic v2 実践ガイド:システム境界を型で守り、信頼できるデータだけを通す

Pydantic v2公式ドキュメントに忠実に、BaseModel/Fieldの宣言的モデル、field_validator/model_validator、model_dump、ConfigDictとstrictモード、pydantic-settings、v1移行までを境界バリデーションの実務観点で解説します。

Published
Reading time
18 min read
Author
友田 陽大
Chia sẻ

導入:なぜ今「Pydantic v2」を学び直すべきなのか

堅牢なバックエンドの設計思想は、たった一文に集約できます——「システム境界の外から来るデータを、決して信頼しない」。HTTP リクエストボディ、外部 API のレスポンス、環境変数、メッセージキューのペイロード。これらはすべて「型の保証がない、検証されていないデータ」であり、アプリケーションの内側に素通しさせた瞬間、KeyErrorAttributeError、そして最悪の場合はセキュリティホールへと姿を変えます。

Pydantic は、この境界に立つ門番です。FastAPI が事実上の標準フレームワークになった今、Python における境界バリデーションの中核はほぼ Pydantic に収束しました。しかし問題があります。Web 上の記事・Stack Overflow・生成 AI が出力するコードの多くが、いまだに v1 系のレガシースタイル@validator.dict()class Config)で書かれているのです。

2023 年 6 月末に正式リリースされた Pydantic v2 は、単なるバージョンアップではありませんでした。バリデーションの中核エンジンが pydantic-core として Rust で書き直され、別パッケージへ分離。公式が「v1 比で大幅に高速化した」と謳う性能を獲得すると同時に、API も model_* プレフィックスへ刷新されました。v1 の知識でコードを書くと、動きはしても陳腐化した書き方になり、技術的負債になります。

この記事は入門の繰り返しではありません。公式ドキュメント(pydantic.dev/docs/validation/latest)に忠実でありながら、それより一段わかりやすく、実務で必ず直面する以下の壁を具体的なコードで突破します。

  • @validator で書いてきたが、v2 の @field_validator / @model_validator で何が変わったのか分からない」
  • Field() の制約・aliasdefault_factory の使い分けが曖昧」
  • 「パスワード確認のような複数フィールドにまたがる検証をどこに書くべきか」
  • .dict() が動かない。model_dump(mode='json') との違いは?」
  • strict モードと型強制(coercion)、本番ではどちらを選ぶべきか」
  • 「環境変数を os.environ['...'] で散々読んでいるが、型安全に集約したい」

筆者は、経済産業大臣賞を受賞した B2B SaaS のバックエンドを Python 3.11 / Flask / SQLAlchemy 2.0 / PostgreSQL 16 で設計・実装し、Router → UseCase → Repository → Model の厳格な層分離で本番運用してきました。そのプロジェクトの境界バリデーションには Marshmallow 3 を採用しましたが、「外部入力を境界で必ず検証してから内側へ通す」という規律そのものは本記事と完全に同一です。FastAPI ベースのスタックでは、その役割をまさに Pydantic が担います。本記事は、その境界設計の知見を Pydantic v2 公式ドキュメントの裏付けと共に整理したものです。

💡 この記事は Python バックエンド設計の連作の一部です。Web フレームワーク層は FastAPI 本番運用ガイド、永続化層は SQLAlchemy 2.0 実践ガイド を併せて読むと、境界から DB までの一貫した型安全設計が見渡せます。


1. BaseModelField:宣言的に「正しいデータの形」を定義する

Pydantic の出発点は BaseModel の継承です。クラス属性に型アノテーションを書くだけで、それが**スキーマ・バリデーション・シリアライズの単一の真実(Single Source of Truth)**になります。

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str = "Jane Doe"  # デフォルト値を持つ=省略可能フィールド


# dict から検証して生成(型強制が働き、文字列 "42" は int 42 になる)
user = User.model_validate({"id": "42"})
print(user.id)    # 42  ← int に変換されている
print(user.name)  # "Jane Doe"

User(id="42") のようにコンストラクタを直接呼んでも検証は走りますが、外部入力(dict / JSON)からの生成には model_validate() / model_validate_json() を使うのが定石です。境界での「検証して初めて型付きオブジェクトになる」という意図が、コード上で明確になります。

H3: Field() で制約・別名・デフォルトを宣言する

型アノテーションだけでは「正の整数」「3〜30 文字」といった業務上の制約は表現できません。それを担うのが Field() です。

from typing import Annotated
from pydantic import BaseModel, Field


class Product(BaseModel):
    # Annotated パターン(v2 で推奨):型と制約を分離して読みやすい
    name: Annotated[str, Field(min_length=1, max_length=120)]
    price: Annotated[int, Field(gt=0)]                 # 正の整数のみ
    discount_rate: Annotated[float, Field(ge=0, le=1)] # 0.0〜1.0
    sku: Annotated[str, Field(pattern=r"^[A-Z]{3}-\d{4}$")]

    # タグは「都度新しい空リスト」を生成(mutable default の罠を回避)
    tags: list[str] = Field(default_factory=list)

主な制約パラメータは公式ドキュメントどおり次のとおりです。

パラメータ意味適用される型
gt / ge / lt / leより大きい / 以上 / より小さい / 以下数値
min_length / max_length最小・最大長文字列・コレクション
pattern正規表現マッチ文字列
default静的なデフォルト値すべて
default_factoryデフォルトを生成する呼び出し可能オブジェクトすべて

⚠️ mutable default の罠tags: list[str] = [] と書くと、全インスタンスで同じリストオブジェクトを共有してしまう Python 古典のバグになります。Pydantic はこれを検出しますが、コレクションや辞書のデフォルトは必ず default_factory=list / default_factory=dict を使ってください。

H3: alias で「外部の命名」と「内部の命名」を分離する

外部 API が camelCase で、内部コードは snake_case で統一したい——よくある要求です。Field(alias=...) がこの翻訳を担います。

from pydantic import BaseModel, ConfigDict, Field


class ApiPayload(BaseModel):
    # 入力 JSON は "userName" だが、内部では user_name として扱いたい
    model_config = ConfigDict(populate_by_name=True)

    user_name: str = Field(alias="userName")
    is_active: bool = Field(alias="isActive")


# 外部のキャメルケースで検証
payload = ApiPayload.model_validate({"userName": "alice", "isActive": True})
print(payload.user_name)  # "alice"  ← 内部はスネークケース

alias は検証・シリアライズの両方に効きます。検証時とシリアライズ時で別名を使い分けたい場合は validation_alias / serialization_alias を個別に指定します。populate_by_name=True を付けると、別名・フィールド名のどちらでも値を投入できるようになり、移行期の後方互換性に有効です。

なぜこれが優れているのか? 外部スキーマの命名規則がアプリケーション内部のコード品質を侵食しないよう、alias翻訳レイヤーを境界に閉じ込めます。外部 API が突然 user_nameuserId に変えても、修正箇所は Field(alias=...) の一行だけ。これは CLAUDE.md でいう「ETC(Easy To Change)」の実践であり、変更の影響範囲を境界に局所化します。


2. バリデータ:型では表現できないビジネスルールを検証する

Field() の制約は「単一フィールドの静的なルール」までです。「メールアドレスを正規化する」「パスワードと確認用パスワードが一致する」といった動的・複数フィールドにまたがる検証には、バリデータデコレータを使います。

H3: @field_validator:単一フィールドを検証・変換する

@field_validator は特定フィールドの値を受け取り、検証または変換した値を返します。v2 では @classmethod と併用するのが正準です。

from pydantic import BaseModel, field_validator


class SignupForm(BaseModel):
    email: str
    age: int

    @field_validator("email", mode="after")
    @classmethod
    def normalize_email(cls, value: str) -> str:
        # mode="after":Pydantic の内部検証後に走る。value は既に str 型が保証される
        return value.strip().lower()

    @field_validator("age", mode="after")
    @classmethod
    def must_be_adult(cls, value: int) -> int:
        if value < 18:
            raise ValueError("18歳以上である必要があります")
        return value

mode の使い分けが要点です。公式の定義に忠実に整理します。

mode実行タイミング受け取る値主な用途
"after"(デフォルト)Pydantic の内部検証型が保証された値型安全な検証・正規化(第一選択)
"before"内部検証・型強制生の入力(Any入力形式の事前整形(例:単一値をリストに包む)
"wrap"検証の前後を自分で制御Any + handler例外捕捉・フォールバックなど最も柔軟

mode="before" は「DB やフォームから来る雑多な形式」を正規の形に整える前処理に有効です。

from typing import Any
from pydantic import BaseModel, field_validator


class Article(BaseModel):
    tags: list[str]

    @field_validator("tags", mode="before")
    @classmethod
    def ensure_list(cls, value: Any) -> Any:
        # "python,rust" のような単一文字列もリストとして受け入れる
        if isinstance(value, str):
            return [t.strip() for t in value.split(",")]
        return value

💡 mode="after" を優先せよ:公式は after バリデータを「一般により型安全」と位置づけています。before は入力が Any で型保証がないため、必要な前処理に限定し、検証ロジックの大半は after に置くのが安全です。

H3: @model_validator:複数フィールドにまたがる検証

「パスワードと確認用パスワードの一致」のようにフィールド間の関係を検証するには @model_validator を使います。mode="after" ではインスタンスメソッドとして定義し、検証済みの self を返します。

from typing import Self
from pydantic import BaseModel, model_validator


class PasswordChange(BaseModel):
    password: str
    password_repeat: str

    @model_validator(mode="after")
    def check_passwords_match(self) -> Self:
        # この時点で password / password_repeat は型検証済み
        if self.password != self.password_repeat:
            raise ValueError("パスワードが一致しません")
        return self

一方、mode="before" はモデルがインスタンス化されるに生の入力(dict)全体を受け取ります。「特定キーの存在を禁止する」といった入力ガードに向きます。

from typing import Any
from pydantic import BaseModel, model_validator


class Account(BaseModel):
    username: str

    @model_validator(mode="before")
    @classmethod
    def forbid_raw_card_number(cls, data: Any) -> Any:
        # 生のクレジットカード番号が混入していたら即座に拒否する
        if isinstance(data, dict) and "card_number" in data:
            raise ValueError("card_number を直接含めることはできません")
        return data

なぜこれが優れているのか? クロスフィールド検証をルーターやサービス層に手書きの if で散らすと、検証ロジックがビジネスロジックに混入し、SRP(単一責任)が崩れます。@model_validator に集約すれば、「このモデルが表す不変条件(invariant)」がモデル定義の中で完結します。PasswordChange のインスタンスが存在する=パスワードが一致している、という保証がコードレベルで担保され、下流のあらゆるコードがその前提を信頼できます。


3. シリアライズ:型付きオブジェクトを「外向きの形」へ安全に戻す

検証してオブジェクトにしたら、今度はレスポンス JSON や DB 保存形式へ戻す必要があります。Pydantic v2 では model_dump() / model_dump_json() がこれを担います(v1 の .dict() / .json() は廃止)。

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str
    password: str


user = User(id=1, name="alice", password="secret")

# Python オブジェクトの dict(tuple などは Python 型のまま保持される)
user.model_dump()          # {'id': 1, 'name': 'alice', 'password': 'secret'}

# JSON 文字列(datetime → ISO 文字列など JSON 互換型へ変換される)
user.model_dump_json()     # '{"id":1,"name":"alice","password":"secret"}'

model_dump(mode='json')mode='python'(デフォルト)の違いは実務で頻出します。mode='python'tupledatetime を Python 型のまま保持しますが、mode='json' は JSON 互換型(リスト・ISO 文字列など)へ変換します。model_dump_json() は後者を直接 JSON 文字列にしたものと考えると整理できます。

主要な制御パラメータは次のとおりです。

パラメータ効果典型的な用途
exclude={'password'}指定フィールドを除外機密情報をレスポンスから外す
include={'id', 'name'}指定フィールドだけ出力部分的な公開
by_alias=Trueフィールド名でなく alias で出力外部のキャメルケース API へ返す
exclude_none=True値が None のフィールドを除外疎なレスポンス
exclude_unset=True明示的に渡されなかったフィールドを除外PATCH の差分更新

⚠️ 機密情報の漏洩防止:パスワードハッシュやトークンを誤ってレスポンスに含めるのは典型的な事故です。model_dump(exclude={"password"}) を都度書くのは漏れの温床になるため、後述の field_serializer や、そもそも外向き専用のレスポンスモデルを分ける設計が堅牢です。

H3: field_serializer で出力を変換する

特定フィールドの出力形式をカスタマイズするには @field_serializer を使います。

from datetime import datetime
from pydantic import BaseModel, field_serializer


class Event(BaseModel):
    name: str
    starts_at: datetime

    @field_serializer("starts_at")
    def serialize_starts_at(self, value: datetime) -> str:
        # フロントの表示規約に合わせて Unix エポック秒で返す
        return str(int(value.timestamp()))

H3: computed_field で派生値をシリアライズに含める

「他フィールドから計算される値」を出力に含めたいときは @computed_field@property と重ねます。

from pydantic import BaseModel, computed_field


class Box(BaseModel):
    width: float
    height: float
    depth: float

    @computed_field
    @property
    def volume(self) -> float:
        return self.width * self.height * self.depth


box = Box(width=2, height=3, depth=4)
box.model_dump()  # {'width': 2.0, 'height': 3.0, 'depth': 4.0, 'volume': 24.0}

computed_field で宣言した値は model_dump() の出力と JSON Schema(readOnly: True)に含まれます。公式が明記するとおり Pydantic は computed_field に追加の検証ロジックを適用しません——あくまで「派生値の出力」のための仕組みです。

なぜこれが優れているのか? volume を呼び出し側で都度計算すると、同じ計算式が複数箇所に散らばり DRY 違反になります。computed_field はその知識をモデルという単一の場所に閉じ込め、シリアライズ結果として一貫して露出します。データとその派生ロジックが凝集し、変更理由が一点に集約されます。


4. strict モードと型強制:安全性と利便性のトレードオフ

Pydantic はデフォルトで**型強制(coercion / lax モード)**を行います。文字列 "123"int123 に、"true"boolTrue に変換してくれる、これが便利さの源泉です。しかし、この「賢さ」が裏目に出る場面があります。

from pydantic import BaseModel


class Order(BaseModel):
    quantity: int


# lax(デフォルト):文字列が黙って int に変換される
Order.model_validate({"quantity": "5"})   # quantity=5  ← 通ってしまう

決済金額や在庫数のように型の厳密さが事業リスクに直結するフィールドでは、この暗黙変換がバグの温床になります。strict モードは型強制を無効化し、型の完全一致を要求します。

from pydantic import BaseModel, ConfigDict, Field


# ① 呼び出し単位で strict にする
Order.model_validate({"quantity": "5"}, strict=True)
# → ValidationError:str は int として受け付けられない

# ② フィールド単位で strict にする
class StrictOrder(BaseModel):
    quantity: int = Field(strict=True)
    note: str  # ここは lax のまま


# ③ モデル全体を strict にする
class FullyStrictOrder(BaseModel):
    model_config = ConfigDict(strict=True)
    quantity: int
    amount: int

strict の挙動を整理します。

入力lax(デフォルト)strict
{"quantity": "5"}(文字列)5 に変換ValidationError
{"quantity": 5}(整数)55
{"is_active": "true"}True に変換ValidationError

💡 どこで strict を使うか:API の最外周で人間や緩いクライアントから受け取る入力は、利便性重視で lax のままにし、int 化を Pydantic に任せるのが現実的です。一方、サービス内部のドメインモデルや金額・数量など、暗黙変換が事故になる箇所はフィールド単位で strict=True にする。この使い分けが、利便性と安全性のバランスを取る実務的な落としどころです。なお JSON モードでは、strict であっても日時文字列のように「JSON に厳密な型が存在しない」値の変換は許容されます。


5. 設定管理:pydantic-settings で 12-factor を型安全に実現する

環境変数を os.environ["DATABASE_URL"] で都度読むコードは、型が str 固定・存在保証なし・デフォルト値が散在という三重苦を抱えます。pydantic-settings は、設定を型付きの単一モデルに集約し、環境変数から自動ロードします。

⚠️ 別パッケージである点に注意:v2 で BaseSettings は本体から分離され、別パッケージになりました。pip install pydantic-settings が必要で、インポートは from pydantic_settings import ... です(from pydantic import BaseSettings は v1 の書き方で、v2 では動きません)。

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    # .env を読み、APP_ プレフィックス付きの環境変数にマッピングする
    model_config = SettingsConfigDict(
        env_file=".env",
        env_prefix="APP_",
        case_sensitive=False,
    )

    database_url: str                              # APP_DATABASE_URL(必須)
    debug: bool = False                            # APP_DEBUG(型強制で "1"→True)
    allowed_hosts: list[str] = Field(default_factory=list)  # JSON としてパース
    max_connections: int = Field(default=10, gt=0)


# アプリ起動時に一度だけ生成。未設定の必須項目があればここで即座に失敗する
settings = Settings()

ポイントは公式仕様どおり次のとおりです。

  • 必須フィールドの欠落は起動時に ValidationError となり、設定ミスを「本番で初めて気づく」のではなくデプロイ前に検出できる。
  • debug: bool"1" / "true" のような環境変数文字列を型強制で bool に変換する。
  • list / dict などの複雑な型は、環境変数を JSON としてパースする(APP_ALLOWED_HOSTS='["a.com","b.com"]')。
  • env_prefix で名前衝突を防ぎ、env_nested_delimiter(例 __)でネストした設定を FOO__BAR 形式で表現できる。

なぜこれが優れているのか? 設定が型付きモデルに集約されることで、アプリケーションは「設定がそろっている」状態でしか起動できなくなります(Fail Fast)。settings.max_connections は静的に int と分かり、settings.databse_url のようなタイポは型チェッカーが検出します。これは 12-factor App の「設定を環境に格納する」原則を、型安全とシークレット非ハードコードの両立で実現する定石です。シークレットはコードに書かず環境変数経由でこのモデルへ流し込む——CLAUDE.md のセキュリティ原則とも完全に一致します。


6. v1v2 移行:変更点の早見表

既存の v1 コードベースや、生成 AI が出力しがちな v1 スタイルのコードに遭遇したら、次の対応表で機械的に置き換えられます。公式マイグレーションガイドに記載された主要なリネームを整理します。

v1(旧)v2(新)種別
@validator@field_validator単一フィールド検証
@root_validator@model_validatorモデル全体・クロスフィールド検証
.dict().model_dump()dict へのシリアライズ
.json().model_dump_json()JSON 文字列へのシリアライズ
.copy().model_copy()インスタンス複製
.construct().model_construct()検証なし生成
.parse_obj().model_validate()dict / オブジェクトから検証生成
.parse_raw().model_validate_json()JSON 文字列から検証生成
class Config:model_config = ConfigDict(...)モデル設定
.update_forward_refs().model_rebuild()前方参照の解決
__fields__model_fieldsフィールドメタデータ参照
from pydantic import BaseSettingsfrom pydantic_settings import BaseSettings設定(別パッケージ化)
.from_orm(obj).model_validate(obj, ...)from_attributes=TrueORM オブジェクトからの生成

💡 移行の勘所:v2 のメソッドは一貫して model_ プレフィックスを持ちます。これは「User モデルが業務上の dict() という名前のメソッドを持ちたい」といったユーザー定義フィールドとの名前衝突を避ける設計判断です。@validator@field_validator の変換では、@classmethod の付与と mode= の明示も忘れずに。一括変換には公式提供の移行支援ツール(bump-pydantic)も活用できます。

H3: 非モデル型を検証する TypeAdapter

v1 にはなかった便利な仕組みが TypeAdapter です。BaseModel を定義するまでもない list[int]dict のような型を、その場で検証・シリアライズできます。

from pydantic import TypeAdapter

# list[int] を BaseModel なしで検証する
adapter = TypeAdapter(list[int])
adapter.validate_python(["1", "2", "3"])  # [1, 2, 3]  ← 各要素を型強制
adapter.validate_json("[1, 2, 3]")        # [1, 2, 3]
adapter.dump_json([1, 2, 3])              # b'[1,2,3]'  ← bytes を返す点に注意

外部 API が「ユーザーオブジェクトの配列」をトップレベルで返すケースなど、ルート要素がモデルでない場面で TypeAdapter(list[User]) が威力を発揮します。


結論:境界バリデーションを「型システムの一部」へ

Pydantic v2 は、Rust 製の pydantic-core を中核に据え、型アノテーションと深く統合された現代的なバリデーションライブラリです。本記事の要点を再掲します。

  1. BaseModel + Field() で「正しいデータの形」を宣言的に定義し、model_validate() で境界検証する。
  2. @field_validator(単一)/ @model_validator(クロスフィールド) にビジネスルールを集約し、モデルの不変条件を保証する。
  3. model_dump() / model_dump_json() / field_serializer / computed_field で外向きの形を安全に制御する。
  4. strict モードを金額・数量など事故が許されない箇所に適用し、利便性と安全性を使い分ける。
  5. pydantic-settings で設定を型安全に集約し、Fail Fast とシークレット非ハードコードを両立する。
  6. v1 → v2 早見表model_ プレフィックス API へ機械的に移行し、TypeAdapter で非モデル型も検証する。

「動くコード」と「10 年運用できるコード」の差は、信頼できないデータをどこで・どう堰き止めるかという境界設計の積み重ねにあります。Pydantic は、その境界を型システムの一部として宣言的に表現する最良の道具です。

さらなる探求として、公式ドキュメントの以下を本記事の設計観点を念頭に再読することをお勧めします。


型安全なバックエンド設計のご相談

筆者は、ここで解説した「システム境界で外部入力を必ず検証する」という規律を、経済産業大臣賞を受賞した B2B SaaS の本番環境で(Marshmallow 3 による境界バリデーションとして)実装・運用してきました。FastAPI ベースのスタックでは、その役割を Pydantic v2 が担います。型安全な入力検証・設定管理・API スキーマ設計・外部連携の境界防御といった、事業の信頼性に直結する基盤を、生成 AI を活用して高速かつ高品質に構築します。Python を用いたバックエンド開発・既存システムの型安全化について、お気軽にご相談ください。

友田

友田 陽大

Người phát triển sản phẩm đoạt Giải thưởng Bộ trưởng (METI). Với TypeScript + Python + AWS, tôi một mình triển khai trọn gói SaaS, chuyển đổi số công nghiệp và AI tạo sinh (RAG) sẵn sàng cho production — từ phân tích yêu cầu đến hạ tầng và vận hành.

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

Nhận cả dự án trọn gói lẫn vai trò cố vấn kỹ thuật. Hãy bắt đầu bằng buổi tư vấn miễn phí 30 phút.

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

Cũng đáng đọc