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` というモデルがあると仮定します。
`manufactured_date`: 1980-01-01 以上でなければならない
`number_of_doors`: 2 または 4 でなければならない(つまり `ge=2, le=4, multiple_of=2`)
`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` 定義だけで強力なバリデーションルールを組み込み、堅牢で安全、かつ一貫したデータモデルを作れます。もちろん、さらに独自のロジックが必要ならカスタムバリデータを使うことも可能ですが、多くの場合はその手間すら不要になるはずです。