2026年7月24日

2026年7月24日

PythonのPydanticでデータバリデーションする方法|v2のBaseModel・バリデータ・エラー処理・設定の読み込み

Pydantic は、Python の型ヒントで「データの形」を宣言しておくと、受け取った値をその形に検証・変換してくれるライブラリです。API のレスポンス、Webhook で届く JSON、設定ファイルや環境変数など、外から入ってくるデータを最初に BaseModel に通しておけば、その先のコードは「正しい形のデータ」だけを前提に書けます。この記事では、現在主流の Pydantic v2 を前提に、モデルの定義、WordPress の REST API のレスポンスを検証する例、独自のバリデータ、エラーの読み方、環境変数からの設定の読み込み、v1 から移行するときの書き換えまでを説明します。

インストールとバージョンの確認

pip install pydantic
python -c "import pydantic; print(pydantic.VERSION)"

表示が 2. で始まれば v2 です。v1 と v2 ではメソッド名やバリデータの書き方が大きく違い、ネット上には v1 の書き方の記事も多く残っています。コードを参考にするときは、どちらのバージョンの書き方かを確認してください。この記事のコードは Pydantic 2 系で動作を確認しています(2026年10月現在)。

基本:モデルを定義して検証する

WordPress の REST API(/wp-json/wp/v2/posts)が返す投稿データのうち、使う項目だけをモデルにします。

from datetime import datetime

from pydantic import BaseModel, Field, HttpUrl


class Rendered(BaseModel):
    rendered: str


class Post(BaseModel):
    id: int
    date: datetime
    slug: str
    link: HttpUrl
    title: Rendered
    categories: list[int] = Field(default_factory=list)

title は API では {"rendered": "..."} という入れ子のオブジェクトなので、別のモデル Rendered を型として指定しています。入れ子の構造はこのようにモデルを組み合わせて表します。

検証は model_validate() に辞書を渡して行います。

data = {
    "id": "12",
    "date": "2026-10-01T09:00:00",
    "slug": "hello",
    "link": "https://example.com/hello/",
    "title": {"rendered": "はじめての投稿"},
    "author": 1,
}
post = Post.model_validate(data)
print(post.id + 1)        # 13(文字列 "12" が int に変換されている)
print(post.date.year)     # 2026(datetime 型になっている)
print(post.title.rendered)

ここで起きていることは3つです。

  • 型の変換:"12" のような数字の文字列は int に、ISO 8601 形式の日時文字列は datetime に変換されます。これを「lax(寛容)モード」と呼びます。
  • 余分な項目の扱い:モデルに無い author は、既定では黙って無視されます。API のレスポンスには項目が多いので、使う項目だけを定義できるのは便利です。
  • 既定値:categories が無い場合は空のリストになります。リストや辞書の既定値は default_factory で指定すると、意図がはっきりします。

なお、WordPress の REST API の date はサイトのタイムゾーンでの日時で、タイムゾーンの情報が付いていません。UTC で扱いたい場合は date_gmt を使い、UTC として解釈してください。

API のレスポンスをまとめて検証する

記事一覧のように「モデルのリスト」を検証するときは TypeAdapter を使います。

import requests
from pydantic import TypeAdapter, ValidationError

PostList = TypeAdapter(list[Post])

res = requests.get(
    "https://example.com/wp-json/wp/v2/posts",
    params={"per_page": 10},
    timeout=(5, 20),
)
res.raise_for_status()

try:
    posts = PostList.validate_json(res.content)
except ValidationError as e:
    print(f"想定と違うレスポンスです({e.error_count()} 件の問題)")
    raise

for p in posts:
    print(p.date.date(), p.slug, p.link)

validate_json() は JSON のバイト列や文字列を直接受け取り、解析と検証を一度に行います。res.json() で辞書にしてから validate_python() に渡しても同じ結果ですが、JSON から直接検証するほうが速く動きます。

レスポンスを検証しておくと、プラグインの更新やサイト側の設定変更で API の形が変わったときに、「どこかで KeyError が出る」のではなく「link が URL ではない」のように、入口で原因のわかるエラーになります。

エラーの読み方

検証に失敗すると ValidationError が発生します。errors() は、問題ごとに場所(loc)・種類(type)・メッセージ(msg)を持つ辞書のリストを返します。

try:
    Post.model_validate({"id": "abc", "date": "x", "slug": "s", "link": "nope", "title": {}})
except ValidationError as e:
    for err in e.errors():
        print(err["loc"], err["type"], err["msg"])
('id',) int_parsing Input should be a valid integer, unable to parse string as an integer
('date',) datetime_from_date_parsing Input should be a valid datetime or date, input is too short
('link',) url_parsing Input should be a valid URL, relative URL without a base
('title', 'rendered') missing Field required

Pydantic は最初のエラーで止まらず、すべての問題をまとめて報告します。loc は入れ子の位置をタプルで表すので、('title', 'rendered') なら「title の中の rendered が無い」という意味です。ログに残すときやユーザーに返すときは、type で分岐すると、英語のメッセージに頼らずに処理を変えられます。

エラーには入力された値(input)も含まれます。パスワードや個人情報を含むデータを検証している場合、str(e) や errors() をそのままログに出すと、その値まで記録されることがあるので注意してください。

制約とバリデータ

値の範囲や長さの制約

数値の範囲や文字列の長さは、Annotated と Field で宣言できます。

from datetime import date
from typing import Annotated

from pydantic import BaseModel, ConfigDict, Field, HttpUrl, field_validator, model_validator


class ReportConfig(BaseModel):
    model_config = ConfigDict(extra="forbid")

    site_url: HttpUrl
    per_page: Annotated[int, Field(ge=1, le=100)] = 10
    start: date
    end: date
    slugs: list[str] = Field(default_factory=list)

    @field_validator("slugs")
    @classmethod
    def normalize_slugs(cls, v: list[str]) -> list[str]:
        cleaned = [s.strip().lower() for s in v if s.strip()]
        if len(cleaned) != len(set(cleaned)):
            raise ValueError("スラッグが重複しています")
        return cleaned

    @model_validator(mode="after")
    def check_range(self) -> "ReportConfig":
        if self.start > self.end:
            raise ValueError("start は end 以前の日付にしてください")
        return self
  • **ge / le**:「以上」「以下」です。gt / lt(より大きい・より小さい)、文字列なら min_length / max_length、正規表現なら pattern も使えます。
  • **extra="forbid"**:モデルに無い項目が来たらエラーにします。設定ファイルの検証では、per_page を per_pag と打ち間違えたような場合に「黙って既定値が使われる」のを防げるので、こちらが向いています。API のレスポンスのように項目が増えることがあるデータには、既定の無視する動作が向いています。

field_validator と model_validator

@field_validator は1つの項目に対する独自の検証や整形です。ValueError を投げると検証エラーになり、値を返すとその値がモデルに入ります。上の例では、前後の空白を除いて小文字にそろえたうえで、重複を検出しています。@classmethod を付けるのが v2 の書き方です。

@model_validator(mode="after") は、すべての項目の検証が終わったあとに呼ばれ、項目どうしの関係を確認できます。「開始日が終了日より後になっていないか」のような検証はここに書きます。self を返すのを忘れないでください。

型変換をさせたくないとき(strict モード)

lax モードは便利ですが、「文字列の "1" が来たら、それ自体が不具合のサイン」という場面もあります。model_config = ConfigDict(strict=True) をモデルに設定するか、特定の項目だけ Field(strict=True) にすると、型が一致しない値はエラーになります。外部から来るデータの形をしっかり固定したい場合に使います。

環境変数から設定を読み込む(pydantic-settings)

スクリプトの設定を環境変数や .env ファイルから読み込むには、別パッケージの pydantic-settings を使います。v1 では Pydantic 本体に含まれていた BaseSettings は、v2 でこちらに移りました。

pip install pydantic-settings
from pydantic import HttpUrl, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict


class AppSettings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="WPBOT_", env_file=".env")

    site_url: HttpUrl
    username: str
    app_password: SecretStr
    timeout: float = 10.0


settings = AppSettings()  # WPBOT_SITE_URL などの環境変数や .env から読み込む
print(settings)           # app_password は '**********' と表示される
auth = (settings.username, settings.app_password.get_secret_value())

.env には次のように書きます(値はダミーです)。

WPBOT_SITE_URL=https://example.com
WPBOT_USERNAME=editor-bot
WPBOT_APP_PASSWORD=your-application-password
  • **SecretStr**:print() やログ、エラーメッセージに値がそのまま出ないようにする型です。実際の値が必要な箇所でだけ get_secret_value() を呼びます。
  • **.env の管理**:.env は Git にコミットせず、.gitignore に追加します。Web サーバーの公開ディレクトリにも置かないでください。
  • WordPress のアプリケーションパスワード:REST API に書き込みをする自動化では、ログイン用のパスワードではなく、ユーザーのプロフィール画面で発行するアプリケーションパスワードを使い、必要最小限の権限のユーザーに発行します。

設定値が足りない・型が違う場合は、起動時に ValidationError になるので、処理の途中で設定の不備に気づく事態を防げます。

HttpUrl は文字列ではなく URL 型として保存され、https://example.com のようにパスの無い URL には末尾に / が付きます。他のライブラリに渡すときは str(settings.site_url) で文字列にし、パスをつなげるときは / が二重にならないよう注意してください。

モデルを辞書や JSON に戻す

検証したモデルを保存したり別の API に送ったりするときは、model_dump() と model_dump_json() を使います。

post.model_dump()              # Python の辞書(datetime は datetime のまま)
post.model_dump(mode="json")   # JSON にできる値に変換した辞書(datetime は文字列)
post.model_dump_json(indent=2) # JSON 文字列

exclude_none=True で値が None の項目を除いたり、include / exclude で出力する項目を選んだりできます。

v1 から v2 への主な書き換え

古いコードを v2 で動かすときに、よく書き換えが必要になる箇所です。

v1 v2
Model.parse_obj(data) Model.model_validate(data)
Model.parse_raw(text) Model.model_validate_json(text)
obj.dict() / obj.json() obj.model_dump() / obj.model_dump_json()
@validator @field_validator(@classmethod を付ける)
@root_validator @model_validator
class Config: model_config = ConfigDict(...)
from pydantic import BaseSettings from pydantic_settings import BaseSettings

v1 の書き方の多くは v2 でも警告付きで動くことがありますが、将来のバージョンで削除される予定です。移行期間中は、v2 に同梱されている pydantic.v1 から import すれば v1 の動作のまま使えます。一度にすべてを書き換えず、テストを用意してからモデル単位で移行すると安全です。

まとめ

  • 外から入ってくるデータは、使う直前ではなく入口で BaseModel に通し、以降は検証済みのモデルだけを扱う
  • model_validate() は辞書、model_validate_json() は JSON 文字列を検証する。リストは TypeAdapter(list[Model]) を使う
  • ValidationError.errors() の loc と type で問題の場所と種類がわかる。機密情報を含む入力はエラーのログ出力に注意する
  • 範囲や長さは Field で宣言し、独自の検証は @field_validator、項目どうしの関係は @model_validator(mode="after") に書く。設定ファイルは extra="forbid" で打ち間違いを検出する
  • 環境変数や .env の設定は pydantic-settings の BaseSettings で読み込み、パスワードは SecretStr で扱う

お気軽にご相談ください

お見積りへ お問い合わせへ
▲