見出し画像

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 つの組み合わせ

  1. 必須・非ヌル

   class M(BaseModel):
       field: int  # 必須だし None は許されない
  1. 必須・ヌル許可

   class M(BaseModel):
       field: int | None  # 値の指定は必須だが、None も受け取る
  1. オプション・非ヌル

   class M(BaseModel):
       field: int = 0  # 値を与えなければ 0 が入る、与えれば int
  1. オプション・ヌル許可

   class M(BaseModel):
       field: int | None = None

特に「値を指定しなければデフォルトが None、指定すれば int か None」を許す、というケースでよく使われます。


モデルのフィールドと値を調べる

これまで `model_dump()` でフィールドをシリアライズする方法を見てきましたが、Pydantic には他にも興味深いプロパティがあります。

  1. `SomeModel.model_fields`
    すべてのフィールド定義情報(型、デフォルトなど)が格納されたディクショナリ。

  2. `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` モデルを作成します:

  1. `manufacturer`: `str`(必須、ヌル不可)

  2. `series_name`: `str`(必須、ヌル不可)

  3. `type_`: `str`(必須、ヌル不可)

  4. `is_electric`: `bool`(デフォルト `False`、ヌル不可)

  5. `manufactured_date`: `datetime.date`(必須、ヌル不可)

  6. `base_msrp_usd`: `float`(必須、ヌル不可)

  7. `vin`: `str`(必須、ヌル不可)

  8. `number_of_doors`: `int`(デフォルト `4`、ヌル不可)

  9. `registration_country`: `str | None`(デフォルト `None`)

  10. `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

テスト:

  1. デシリアライズ: 辞書や JSON からインスタンスを作る。

  2. シリアライズ: `.model_dump()` などで辞書に戻し、期待する辞書と比較。

  3. バリデーション: フィールドを欠落させたり型を間違えたりして `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)や高度なシリアライズ・デシリアライズ、カスタムバリデータなどを通じて、モデルをより洗練させていきます。どうぞお楽しみに!

「超温和なパイソン」へ

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