Pydantic V2: Essentials: バリデーション (セクション2-2/13)
必須/オプション・ヌル可/不可の組み合わせとデフォルト値の扱い方を学び、フィールドの柔軟な定義が可能になる。
`model_fields_set`などを使って、ユーザが実際に設定した値とデフォルトの見分けがつけられる。
JSONスキーマの自動生成や、プロジェクトとしての`Automobile`モデル構築を通じて、現実的なデータ検証とシリアライズ手法を習得する。
前半のレッスンでは、Pydantic モデルの作成方法、シリアライズ(model → dict/JSON)や デシリアライズ(dict/JSON → model)について学び、Pydantic がどのように 型の変換(type coercion) を扱うかを説明しました。このセクション 2 の後半では、必須フィールド vs. オプションフィールド、ヌル許可(nullable)フィールドの扱い、何が指定され何がデフォルトなのかを確認する方法、JSON スキーマ生成、そしてすべてを結びつけるミニプロジェクトを紹介します。
必須フィールドとオプションフィールドの作り方
Pydantic のフィールドは、必須(入力データに必ず存在すべき)か オプション(なければデフォルト値が使われる)のいずれかです。
from pydantic import BaseModel
class Circle(BaseModel):
center: tuple[int, int] = (0, 0) # オプション、デフォルト (0, 0)
radius: int # 必須
オプションのフィールド: デフォルトを指定する。
必須のフィールド: デフォルトを指定しない。Pydantic は必須として取り扱う。
注意:デフォルト値は基本的に検証されない
標準動作として、Pydantic はデフォルト値を与えるときにその妥当性チェックを行いません。たとえば、`radius` に文字列 `"Python"`(整数ではない)をデフォルトで設定しても、警告が出ません。これはランタイムのオーバーヘッドを下げるための設計です。
ミュータブルなデフォルト
Python では、関数やクラスでミュータブル(変更可能な)オブジェクトをデフォルトにすると、意図せず複数のインスタンスで共有されることがあります。dataclasses では “default factory” を使って回避します。Pydantic は自動で ディープコピー をとることで、ミュータブルなオブジェクトを各インスタンスごとに独立させる仕組みを持っています。
ヌル許可フィールド(Nullable)とオプションフィールド
オプションフィールド: 入力データに無くてもよい(無ければ デフォルト が使われる)。
ヌル許可フィールド(nullable): フィールドに `None` を明示的にセットしてもよい。
from pydantic import BaseModel
class Model(BaseModel):
field: int | None # 入力は int か None
`None` を許可しつつ、データが存在しなければデフォルトを使いたい場合は、デフォルトに `None` を設定し、型を `int | None` とすると便利です。
class Model(BaseModel):
field: int | None = None
欠落 → デフォルトの `None` が使われる
値が "field": null → 実際に `None` がセットされる
普通に整数を指定 → 整数になる
よくある落とし穴:型が int なのにデフォルトが None
class BrokenModel(BaseModel):
field: int = None # デフォルトは None でも型は int のまま
Pydantic はデフォルト値を検証しないため、見た目は動くように見えますが、実際に `"field": null` を与えるとエラーになります。正しくは `int | None = None` と書きましょう。
必須・オプションとヌル許可・非ヌルの 4 つの組み合わせ
必須・非ヌル
class M(BaseModel):
field: int # 必須だし None は許されない
必須・ヌル許可
class M(BaseModel):
field: int | None # 値の指定は必須だが、None も受け取る
オプション・非ヌル
class M(BaseModel):
field: int = 0 # 値を与えなければ 0 が入る、与えれば int
オプション・ヌル許可
class M(BaseModel):
field: int | None = None
特に「値を指定しなければデフォルトが None、指定すれば int か None」を許す、というケースでよく使われます。
モデルのフィールドと値を調べる
これまで `model_dump()` でフィールドをシリアライズする方法を見てきましたが、Pydantic には他にも興味深いプロパティがあります。
`SomeModel.model_fields`
すべてのフィールド定義情報(型、デフォルトなど)が格納されたディクショナリ。`some_instance.model_fields_set`
実際に入力から値が設定されたフィールド名の集合。
例:デフォルト由来かユーザ入力由来かを区別
from pydantic import BaseModel
class Circle(BaseModel):
center_x: int = 0
center_y: int = 0
radius: int = 1
name: str | None = None
c1 = Circle(radius=2)
c1.model_fields_set
# {'radius'}
set(c1.model_fields.keys()) - c1.model_fields_set
# {'center_x', 'center_y', 'name'}
ユーザが送ったデータだけを返したい場合などで役立ちます:
c1.model_dump(include=c1.model_fields_set)
これで、明示的にセットされたフィールドだけをシリアライズできます。
JSON スキーマ生成
Pydantic はモデルから自動的に JSON Schema を生成できます。これは FastAPI などの API フレームワークが Swagger/OpenAPI ドキュメントを作成するときに活用されます。
from pydantic import BaseModel
class ExampleModel(BaseModel):
field_1: int | None = None
field_2: str = "Python"
schema = ExampleModel.model_json_schema()
print(schema)
返ってくるのは例えば:
{
"title": "ExampleModel",
"type": "object",
"properties": {
"field_1": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"default": null,
"title": "Field 1"
},
"field_2": {
"type": "string",
"default": "Python",
"title": "Field 2"
}
}
}
パラメータを追加してスキーマを微調整することも可能です。より高度なカスタマイズもできますが、本コースの範囲外です。
プロジェクト:Automobile モデルの構築
目的
以下のフィールドを持つ `Automobile` モデルを作成します:
`manufacturer`: `str`(必須、ヌル不可)
`series_name`: `str`(必須、ヌル不可)
`type_`: `str`(必須、ヌル不可)
`is_electric`: `bool`(デフォルト `False`、ヌル不可)
`manufactured_date`: `datetime.date`(必須、ヌル不可)
`base_msrp_usd`: `float`(必須、ヌル不可)
`vin`: `str`(必須、ヌル不可)
`number_of_doors`: `int`(デフォルト `4`、ヌル不可)
`registration_country`: `str | None`(デフォルト `None`)
`license_plate`: `str | None`(デフォルト `None`)
実装例
from pydantic import BaseModel
from datetime import date
class Automobile(BaseModel):
manufacturer: str
series_name: str
type_: str
is_electric: bool = False
manufactured_date: date
base_msrp_usd: float
vin: str
number_of_doors: int = 4
registration_country: str | None = None
license_plate: str | None = None
テスト:
デシリアライズ: 辞書や JSON からインスタンスを作る。
シリアライズ: `.model_dump()` などで辞書に戻し、期待する辞書と比較。
バリデーション: フィールドを欠落させたり型を間違えたりして `ValidationError` が出るか確認。
サンプルテスト
# data (辞書形式)
data = {
"manufacturer": "BMW",
"series_name": "M4",
"type_": "Convertible",
"is_electric": False,
"manufactured_date": "2023-01-01",
"base_msrp_usd": 93300,
"vin": "1234567890",
"number_of_doors": 2,
"registration_country": "France",
"license_plate": "AAA-BBB"
}
auto = Automobile(**data)
print("デシリアライズしたモデル:", auto)
serialized = auto.model_dump()
print("辞書にシリアライズ:", serialized)
# `expected_serialization` と比較するなら:
# assert serialized == expected_serialization
JSON でのテストも同様です:
import json
data_json = '''
{
"manufacturer": "BMW",
"series_name": "M4",
"type_": "Convertible",
"manufactured_date": "2023-01-01",
"base_msrp_usd": 93300,
"vin": "1234567890"
}
'''
auto_json = Automobile(**json.loads(data_json))
print("JSON から生成:", auto_json)
print("再度シリアライズ:", auto_json.model_dump())
こうして、セクションのプロジェクトとしての基礎的な `Automobile` モデルは完成です。今後のセクションで、さらに高度なバリデーションやフィールド、設定項目を追加し、より洗練されたモデルへと発展させていきます。
まとめ & 次のステップ
ここまでで学んだこと:
オプション化: フィールドにデフォルト値を与えるだけでオプション化可能。
ヌル許可: `int | None` のように定義すると `None` を受け付ける。
何が指定されデフォルトなのか: `model_fields_set` でユーザ入力を確認可能。
JSON スキーマの自動生成: FastAPI などで使われる OpenAPI 文書に役立つ。
リアルなモデル(Automobile)を実装: 欠落や型不一致に対するバリデーション、デフォルト値の反映などを実践。
この基礎を活かせば、
データ構造を正確に定義しつつ、
適切にバリデーションを行い、
必要に応じて `None` やデフォルトで柔軟に対応しながら、
実際の入力状況をコントロールできます。
次のセクションでは、さらに厳密な型検証(strict vs. lax mode)や高度なシリアライズ・デシリアライズ、カスタムバリデータなどを通じて、モデルをより洗練させていきます。どうぞお楽しみに!