見出し画像

Pydantic V2: Essentials: デフォルト (セクション6/13)

  • フィールド定義のみで数値・文字列・パターンなどの制約や動的なデフォルトを設定可能。

  • モデル全体またはフィールド単位で厳密型チェック、デフォルト値のバリデーション、フィールドの凍結などを柔軟に制御できる。

  • シリアライズ時の除外設定(`exclude=True`)も含め、追加のカスタムバリデータなしでも強力なバリデーションを実現できる。

ここまで、Pydantic の `Field` オブジェクトを エイリアス や デフォルト のために使う方法を見てきました。しかし、数値範囲・文字列長・パターンなどを、フィールド定義上で直接 enforce(強制)できることはご存じでしたか? セクション 6:追加のフィールド機能 では、しばしば見過ごされがちですが、毎回カスタムバリデータを書くことなくモデルのバリデーションを洗練する上で非常に便利なツール群を紹介します。

以下にそれらの機能を解説します。


1. 数値に対する制約

数値範囲をフィールド上で直接指定できます。たとえば、整数が「`2` より厳密に大きく、かつ最大 `10` まで、なおかつ `2` の倍数でなければならない」場合は、次のように書きます。

from pydantic import BaseModel, Field

class NumberModel(BaseModel):
    number: float = Field(gt=2, le=10, multiple_of=2)
  • `gt=2`: `2` より厳密に大きい

  • `le=10`: `10` 以下

  • `multiple_of=2`: 2 の倍数でなければならない

:

NumberModel(number=4)  # OK: 4 > 2, <= 10, かつ 2 の倍数
NumberModel(number=3)  # エラー: 2 の倍数ではない
NumberModel(number=14) # エラー: 14 > 10

特殊化された型との等価性

Pydantic には `PositiveInt` のように、内部的には `int` + `Field(gt=0)` と等価な特殊型が用意されています。しかし、いつでも `Field(...)` を使って独自の制約を作成することも可能です。


2. 文字列(およびシーケンス)に対する制約

最小/最大長

`min_length` と `max_length` を使うことで、長さの制限を定義できます。

from pydantic import BaseModel, Field

class NameModel(BaseModel):
    name: str = Field(min_length=1, max_length=20)
  • `min_length=1`: 空文字列を許可しない

  • `max_length=20`: 20 文字を超える名前を許可しない

また、この制約は シーケンス(リストやタプルなど)にも適用されます。

class ItemsModel(BaseModel):
    items: list[float] = Field(min_length=3, max_length=5, default=[1.0, 2.0, 3.0])

:

  • `items=[1.0, 2.0, 3.0]` → OK (3要素)

  • `items=[2.0, 3.0]` → エラー (要素数が少なすぎる)

  • `items=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]` → エラー (要素数が多すぎる)

正規表現パターン

文字列が特定のパターンに合致するかどうかを確かめたいときは、`pattern=` を使います。

import re
from pydantic import BaseModel, Field

class ZipCodeModel(BaseModel):
    zip_code: str = Field(pattern=r"^[0-9]{5}(?:-[0-9]{4})?$")
  • 正規表現: アメリカ式の ZIP コード(`12345` や `12345-1234`)が有効かどうかを確認します。


3. デフォルトファクトリ

Python でよくある落とし穴として、リストや日付時刻などのデフォルト値は、関数やクラスが読み込まれるタイミング(いわばコンパイル時)に一度だけ作成されます。もしインスタンスごとに 新鮮な デフォルトや、動的なデフォルト(例:「現在時刻」)が必要なら、必ず `default_factory` を使わなければなりません。

なぜ必要か

例:タイムスタンプを常に最新に

from datetime import datetime, timezone
from pydantic import BaseModel, Field

class Log(BaseModel):
    dt: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
    message: str
  • `Log()` を作るたび、ラムダが呼び出されて 新しい `datetime.now(timezone.utc)` が返されます。


4. フィールドレベルの追加設定

厳密(Strict)か、緩やか(Lax)か

モデル単位 で以下のように設定できます。

class Model(BaseModel):
    model_config = {"strict": True}
    bool_field: bool

この状態だと、ブール型以外の入力を与えるとエラーになります。もしフィールドごとに挙動を変えたい場合は、次のようにオーバーライドします。

class MixedModel(BaseModel):
    model_config = {"strict": False}  # デフォルトは緩やか
    # 特定のフィールドだけ厳密にする
    strict_bool: bool = Field(strict=True)
    normal_bool: bool = Field(strict=False)

デフォルト値のバリデーション

デフォルト値そのものは、通常 Pydantic では検証されません。これをモデル全体で有効にするには:

class Model(BaseModel):
    model_config = {"validate_default": True}
    num: int = Field(ge=5, default=3)  # default=3 が <5 の場合エラーになる

あるいは、フィールドごとに `validate_default=True/False` で上書きできます。

フィールドの凍結(読み取り専用化)

個々のフィールドを読み取り専用にする:

class Item(BaseModel):
    id: int = Field(frozen=True)
    name: str

item = Item(id=1, name="Widget")
item.name = "Thing"  # OK
item.id = 2          # エラー: フィールドが凍結されている

あるいはモデル全体を凍結する:

class FrozenModel(BaseModel):
    model_config = {"frozen": True}
    a: int
    b: int

m = FrozenModel(a=10, b=20)
m.b = 30  # エラー: インスタンス全体が凍結されている

シリアライズからフィールドを除外する

シリアライズ出力(JSON など)に 絶対に表示したくないデータ(認証情報など)がある場合は、フィールドに `exclude=True` を指定します。

class SecretModel(BaseModel):
    secret_key: str = Field(default="ABC123", exclude=True)
    other_field: int = 99

m = SecretModel()
m.model_dump()  # {'other_field': 99} だけを返し、secret_key は省略される

`model_dump` や `model_dump_json` で、`secret_key` はどんな場合も出力されません。`include=` を使っても“取り戻す”ことはできません。


セクション 6 プロジェクト例

ここまでの内容をすべて活用するとしましょう。`Automobile` というモデルがあると仮定します。

  1. `manufactured_date`: 1980-01-01 以上でなければならない

  2. `number_of_doors`: 2 または 4 でなければならない(つまり `ge=2, le=4, multiple_of=2`)

  3. `id_`: Null 不可、インスタンスごとに一意の `UUID4` がデフォルト

実装スケッチ:

from datetime import date
from enum import Enum
from pydantic import BaseModel, Field, UUID4, ConfigDict
from uuid import uuid4

class AutomobileType(Enum):
    sedan = "Sedan"
    coupe = "Coupe"
    convertible = "Convertible"
    suv = "SUV"
    truck = "Truck"

class Automobile(BaseModel):
    model_config = ConfigDict(
        extra="forbid",
        str_strip_whitespace=True,
        validate_default=True,
        validate_assignment=True,
    )
    id_: UUID4 = Field(alias="id", default_factory=uuid4)  # 常にユニーク
    manufacturer: str
    series_name: str
    type_: AutomobileType = Field(alias="type")
    is_electric: bool = False

    # manufactured_date ≥ 1980-01-01
    manufactured_date: date = Field(ge=date(1980, 1, 1))

    # float または int(JSON から)を受け取れるが、シリアライズ時に "baseMSRPUSD" として出力
    base_msrp_usd: float = Field(serialization_alias="baseMSRPUSD")

    vin: str

    # ドア数は 2 あるいは 4 のみ
    number_of_doors: int = Field(
        default=4,
        ge=2,
        le=4,
        multiple_of=2
    )

    registration_country: str | None = None
    license_plate: str | None = None

    # (オプション)日付をカスタムシリアライズする例
    def model_dump(self, *, by_alias: bool = False, **kwargs):
        data = super().model_dump(by_alias=by_alias, **kwargs)
        if by_alias and "manufactured_date" in data:
            # 日付を "YYYY/MM/DD" フォーマットに変換
            data["manufacturedDate"] = data.pop("manufactured_date").strftime("%Y/%m/%d")
        return data
  • ドア数: `ge=2, le=4, multiple_of=2` で制限

  • ID: `default_factory=uuid4` により、毎回新しい UUID を生成

  • 製造日: `Field(ge=...)` で 1980-01-01 以上を必須化

簡単なテスト:

auto = Automobile(
    id="abcdef12-3456-7890-abcd-ef1234567890",
    manufacturer="BMW",
    series_name="M4",
    type_="Convertible",
    is_electric=False,
    manufactured_date=date(2023,1,1),
    base_msrp_usd=93300.0,
    vin="1234567890",
    number_of_doors=2
)
print(auto.model_dump(by_alias=True))
# "id", "manufacturer", "seriesName", "type" などのキーを含む辞書を出力
# "manufacturedDate": "2023/01/01", "baseMSRPUSD": 93300.0, 等々が表示されるはず

もし `number_of_doors=3` や `manufactured_date=date(1970,1,1)` といった値を与えれば、バリデーションエラーになります。


まとめ

フィールドレベルの設定を活用すると、追加のカスタムバリデータを書くことなく、細かいバリデーションを実現できます。主なポイントは以下のとおりです。

  • 数値の制約: `gt`, `ge`, `lt`, `le`, `multiple_of`

  • 文字列/シーケンスの制約: `min_length`, `max_length`, `pattern`

  • デフォルトファクトリ: `datetime.now()` や新しいリストを生成するなど、都度新しい 値が必要な場合に必須

  • 厳密(Strict) vs. 緩やか(Lax): モデル全体、あるいはフィールド単位で型強制の挙動を切り替え

  • フィールドの凍結: 一部だけ不変にするオプション

  • シリアライズからの除外: `exclude=True` で特定フィールドを常時除外

これらを使いこなせば、Pydantic V2 の `Field` 定義だけで強力なバリデーションルールを組み込み、堅牢で安全、かつ一貫したデータモデルを作れます。もちろん、さらに独自のロジックが必要ならカスタムバリデータを使うことも可能ですが、多くの場合はその手間すら不要になるはずです。

「超温和なパイソン」へ

いいなと思ったら応援しよう!