見出し画像

Pydantic V2: Essentials: 注釈付き型 (セクション10/13)

  • Pydantic V2では、`PlainSerializer`を使って注釈付き型にカスタムシリアライザーを組み込むことで、複数のフィールドに共通のシリアライゼーションロジックを再利用できる。

  • この方法により、日付や時刻のフォーマットなどの処理を一箇所にまとめ、モデル定義をシンプルかつ保守しやすくできる。

  • 一方、従来の`@field_serializer`を用いたフィールドレベルの実装との違いを理解し、用途に応じた適切な手法を選択することが重要である。

Pydantic でシリアライゼーションを扱う際、複数のフィールドにわたってカスタムロジックを定義し再利用することが非常に有用です。すでに、Pydantic モデル内の特定フィールドにカスタムシリアライザーをアタッチするための `@field_serializer` のようなデコレータをご存知かもしれません。しかし、Pydantic V2 では 注釈付き型(Annotated types) にシリアライゼーションロジックを付与することも可能です。このアプローチは、同じシリアライザーを複数のモデルや複数のフィールドで使い回す必要がある場合に特に便利です。

1. 背景: フィールドレベルと注釈付き型レベルのシリアライザーの比較

  • フィールドレベルのデコレータ
    モデルクラス内で `@field_serializer("some_field")` を使用する方法は、カスタムシリアライゼーションが必要な個別のフィールドに対してはシンプルに使えます。しかし、同じシリアライゼーションロジックを複数のフィールド(あるいは複数のモデル)に対して共有する場合、各モデルで同じデコレータを繰り返し記述するのは面倒になってしまいます。

  • 注釈付き型レベルのアプローチ
    ここでは、特定の検証やシリアライゼーションを含むカスタムの Python 型を注釈付き型として作成します。この型は、同じ動作が必要な場所ならどこでも利用可能です。Pydantic がこの注釈付き型のフィールドを見たとき、自動的に定義されたシリアライゼーションロジックを適用します。これにより、コードの再利用性が向上し、保守性も高まります。

2. フィールドシリアライザーのアプローチの再確認

例えば、`datetime` 型のフィールドについて以下の要件があるとします。

  1. `"2020/1/1 3pm"` のような文字列を Python の `datetime` オブジェクトにデシリアライズすること。

  2. 結果は常にタイムゾーン情報付き(UTC)であること。

  3. シリアライズする際、標準の Python 辞書に変換する場合と JSON 文字列に変換する場合で異なる形式で出力すること。

フィールドレベルの解決方法は以下のようになります。

from datetime import datetime
from pydantic import BaseModel, field_serializer, Field
from dateutil.parser import parse
import pytz

# datetime の解析/調整用のヘルパー関数
def parse_datetime(value):
    if isinstance(value, str):
        return parse(value)  # 例: "2020/1/1 3pm" を解析する
    return value

def make_utc(dt: datetime) -> datetime:
    return dt.astimezone(pytz.utc) if dt.tzinfo else pytz.utc.localize(dt)

# JSON 用のカスタムシリアライザー形式
def dt_json_serializer(dt: datetime) -> str:
    return dt.strftime("%Y/%m/%d %I:%M %p UTC")

class MyModel(BaseModel):
    dt: datetime

    # JSON 用(かつフィールドが None でない場合)のカスタムシリアライゼーションロジックをアタッチ
    @field_serializer("dt", when_used="json-unless-none")
    def serialize_dt_json(self, value: datetime) -> str:
        return dt_json_serializer(value)

    # バリデーションの際に、解析と UTC 変換を行うための処理
    @classmethod
    def model_validate(cls, data):
        data['dt'] = make_utc(parse_datetime(data['dt']))
        return super().model_validate(data)

これは問題なく動作しますが、すべてのカスタムロジックがひとつのモデル内に閉じ込められてしまいます。別の場所で再利用する際に、同じ処理をコピー&ペーストしなければならなくなります。

3. 注釈付き型への移行

代替案として、すべての以下の処理をカプセル化する注釈付き型、例えば `DateTimeUTC` を作成します:

  • パース(バリデーション)

  • UTC 変換

  • シリアライゼーション

この注釈付き型を、同じ動作が必要なフィールドに適用することで、Pydantic は自動的に定義済みのシリアライゼーションロジックを実行します。

3.1 バリデーションの注釈付き型への適用

Pydantic V2 では、`BeforeValidator`、`AfterValidator` など複数の検証ステップを型に付与できます。以下のように定義します:

from typing import Annotated
from pydantic import BeforeValidator, AfterValidator

DateTimeUTC = Annotated[
    datetime,
    BeforeValidator(parse_datetime),
    AfterValidator(make_utc),
]

これにより、`DateTimeUTC` 型をフィールドに使用すると、Pydantic はバリデーション前に `parse_datetime` を、バリデーション後に `make_utc` を実行します。

3.2 `PlainSerializer` を用いたシリアライゼーション

同じ注釈付き型にシリアライゼーションロジックを組み込みたい場合、Pydantic では `PlainSerializer` を使用します。`PlainSerializer` は、Pydantic の標準シリアライザーをカスタムのものに置き換えるためのものです。

from pydantic import PlainSerializer

DateTimeUTC = Annotated[
    datetime,
    BeforeValidator(parse_datetime),
    AfterValidator(make_utc),
    PlainSerializer(dt_json_serializer, when_used="json-unless-none"),
]

ここで、`dt_json_serializer` はカスタム関数(例:`lambda dt: dt.strftime("%Y/%m/%d %I:%M %p UTC")`)であり、`when_used="json-unless-none"` は、(1)JSON にダンプする場合、かつ(2)フィールドが None でない場合にカスタムシリアライザーを使用するという条件を示します。

3.3 例: 最小限のモデル

class Model(BaseModel):
    dt: DateTimeUTC  # 自動的にバリデーションとシリアライゼーションが行われる
  • 辞書シリアライゼーション: `Model(...).model_dump()` は標準の Python の `datetime` オブジェクトを出力します。

  • JSON シリアライゼーション: `Model(...).model_dump_json()` は `"2023/01/01 03:00 PM UTC"` のような文字列を出力します。

このパターンは、日付/時刻に関するロジックを一箇所に集約します。複数のモデルやフィールドが同じ動作を必要とする場合、各所でデコレータを繰り返す必要はなくなります。


4. 実際の例: 自動車データ

たとえば、`Automobile` という大規模なモデルがあり、`manufactured_date` と `registration_date` の2つの日付フィールドが存在するとします。両方とも同じ「YYYY/MM/DD」というシリアライゼーション形式が必要な場合、`CustomDate` というカスタム注釈付き型を定義することができます。

from datetime import date
from typing import Annotated
from pydantic import PlainSerializer, FieldSerializationInfo

def serialize_custom_date(value: date, info: FieldSerializationInfo) -> str:
    return value.strftime("%Y/%m/%d")

CustomDate = Annotated[
    date,
    PlainSerializer(serialize_custom_date, when_used="json-unless-none")
]

これにより、`CustomDate` 型を用いるフィールドは自動的にこのシリアライゼーションロジックを持つようになります。例えば、`Automobile` モデル内での使い方は次のようになります。

from pydantic import BaseModel, Field, ConfigDict
from datetime import date

class Automobile(BaseModel):
    model_config = ConfigDict(
        extra="forbid",
        str_strip_whitespace=True,
        validate_default=True,
        validate_assignment=True,
        # その他の設定...
    )

    manufactured_date: CustomDate = Field(
        validation_alias="completionDate",
        ge=date(1980, 1, 1),
        repr=False
    )
    registration_date: CustomDate | None = Field(default=None, repr=False)

    # 他のフィールドおよびロジック...

`CustomDate` 注釈付き型がすでに「YYYY/MM/DD」というフォーマットでシリアライズする方法を知っているため、これらのフィールドに対して個別に `@field_serializer` を定義する必要はありません。コードがよりシンプルになり、各日付フィールドのシリアライゼーションデコレータを重複して記述することがなくなります。

4.1 出力のテスト

auto = Automobile(
    completionDate="2023-01-01",
    registrationDate="2023-06-01",
    # その他のフィールド...
)

print(auto.model_dump(by_alias=True))
# {
#   "manufacturedDate": datetime.date(2023, 1, 1),
#   "registrationDate": datetime.date(2023, 6, 1),
#   ...
# }

print(auto.model_dump_json(by_alias=True))
# {
#   "manufacturedDate": "2023/01/01",
#   "registrationDate": "2023/06/01",
#   ...
# }

JSON 出力では、日付が `"2023/01/01"` や `"2023/06/01"` のような文字列形式になっています。一方、標準の辞書にシリアライズする場合、Pydantic はそれらを Python の `date` オブジェクトとして保持します。


5. 注釈付き型が優れている理由

  1. 再利用性: 複数のモデルにわたって同じシリアライゼーション/バリデーションロジックを書く必要がなく、注釈付き型を一度定義すれば済みます。

  2. 保守性: 日付フォーマットが変更になった場合、注釈付き型内のロジックを一箇所更新すれば、該当するすべてのフィールドが自動的に新しいフォーマットに更新されます。

  3. モデルクラスの簡素化: モデルのロジックはフィールドの定義に専念でき、各フィールドのシリアライゼーションやバリデーションの詳細な実装が散在することがなくなります。


6. `field_serializer` と `PlainSerializer` の比較: 簡単な説明

  • `field_serializer`

    • モデルクラス内でデコレータとして使用できる(複数フィールドに対しても使用可能)。

    • 注釈付き型内でも利用可能ですが、設定(たとえば `when_used` や `return_type`)を明示しない場合、やや暗黙的になります。

  • `PlainSerializer`

    • 主に注釈付き型内で使用され、シリアライザー関数とその設定を一つのオブジェクトとしてカプセル化します。

    • `when_used="json-unless-none"` や `return_type` などのオプションを明示的に指定でき、より明確な設定が可能です。

要点: どちらも同じ目的を果たしますが、文法スタイルに違いがあります。単一モデル内でカスタムロジックを定義する場合は `@field_serializer` で十分ですが、広範に再利用する場合は注釈付き型に `PlainSerializer` を用いることでモデルコードをすっきりと保つことができます。


結論

Pydantic V2 におけるカスタムシリアライザーは、フィールドに直接(`@field_serializer` を用いて)または 注釈付き型 に対して(`PlainSerializer` を用いて)アタッチすることができます。

複数のフィールド、さらには複数のモデルで同一のシリアライゼーション/バリデーションコードを書かなければならない場合、注釈付き型 を用いることで、DRY(Don't Repeat Yourself)の原則に則り、コードの重複を避けることができます。また、モデル自体はビジネスドメインに集中でき、シリアライゼーションのメカニズムが散在しないため、より読みやすく保守性の高いコードになります。

もし大規模なアプリケーションで、共通のデータ変換を行うフィールドが複数存在するなら、その変換処理を注釈付き型にまとめることを検討してください。これにより、作業時間の節約、重複の回避、そしてシリアライゼーションロジックの一元管理が実現できます。

「超温和なパイソン」へ

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