😽

Pythonのpydanticでdatetimeのシリアライズフォーマットを決める

2024/09/02に公開

始めに

Pythonにてアプリケーション内ではdatetimeとして扱いつつ、APIとしてはYYYY-mm-dd等の特定のフォーマットの文字列で返却したいことがあります。

今回はPydanticを用いて実装する方法を記事にします。

環境

  • Python
    • 3.12.4
  • Pydantic
    • 2.8.2

実装

field_serializer を使用する

field_serializerを用いることで、シリアライズ時にフォーマットしてくれるようになります。

次のコードでは、特に何もしていなければISO8601のフォーマットで返却し、field_serializerが有効な個所では%Y-%m-%d %H:%M:%Sで返却するテストを記載しています。

from datetime import datetime
from zoneinfo import ZoneInfo
from pydantic import BaseModel, field_serializer, PlainSerializer
import json
from typing import Annotated

class _TestModel(BaseModel):
    dt: datetime
    normal_dt: datetime

    @field_serializer("dt")
    def serialize_datetime(self, dt: datetime, _info):
        return dt.strftime("%Y-%m-%d %H:%M:%S")


class TestFieldSerializer:
    async def test_01(self):
        dt = datetime(2024, 12, 1, 2, 3, 4, tzinfo=ZoneInfo("UTC"))
        model = _TestModel(dt=dt, normal_dt=dt)
        actual_str = model.model_dump_json()
        actual_dict = json.loads(actual_str)

        assert actual_dict.get("dt") == "2024-12-01 02:03:04"
        assert actual_dict.get("normal_dt") == "2024-12-01T02:03:04Z"

PlainSerializer で型を表現する

AnnotatedPlainSerializerを組み合わせることで型として表現できます。各モデルごとに定義する必要がないので、基本的にはこちらを採用するほうがメリットがあります。

CustomDatetime = Annotated[
    datetime,
    PlainSerializer(lambda dt: dt.strftime("%Y-%m-%d %H:%M:%S"))
]

class _TestModel2(BaseModel):
    dt: CustomDatetime
    normal_dt: datetime


class TestCustomDatetime:
    async def test_serialize(self):
        dt = datetime(2024, 12, 1, 2, 3, 4, tzinfo=ZoneInfo("UTC"))
        model = _TestModel2(dt=dt, normal_dt=dt)
        actual_str = model.model_dump_json()
        actual_dict = json.loads(actual_str)

        assert actual_dict.get("dt") == "2024-12-01 02:03:04"
        assert actual_dict.get("normal_dt") == "2024-12-01T02:03:04Z"

    async def test_deserialize(self):
        # GIVEN
        dt = datetime(2024, 12, 1, 2, 3, 4, tzinfo=ZoneInfo("UTC"))
        model = _TestModel2(dt=dt, normal_dt=dt)
        actual_str = model.model_dump_json()

        # WHEN
        actual = _TestModel2.model_validate_json(actual_str)
        # シリアライズ時にTZが削除されている
        assert actual.dt == dt.replace(tzinfo=None)
        assert actual.normal_dt == dt

ソースコード

終わりに

基本的にはフロントでフォーマットすればいいのですが、全処理をバックエンドに押し付ける場合にはこういう処理も可能です。

また、CSV作成処理等でも流用できます。

参考情報

Discussion