如何在pydantic中更改日期格式

28

如何在 Pydantic 中更改日期格式以进行验证和序列化?对于验证,我正在使用@validator。是否有一种解决方案适用于这两种情况?


1
或许这个链接会有帮助:https://dev59.com/EL7pa4cB1Zd3GeqP04z3#65231106 - alex_noname
1
@alex_noname 在 data.json() 中可以工作,但在 fastapi 序列化器中无法工作。 - jonsbox
6个回答

26

您可以使用 pydantic的自定义json编码器 实现自定义json序列化。然后,与 pydantic的自定义验证器 一起使用,您可以同时具备两种功能。


from datetime import datetime, timezone
from pydantic import BaseModel, validator


def convert_datetime_to_iso_8601_with_z_suffix(dt: datetime) -> str:
    return dt.strftime('%Y-%m-%dT%H:%M:%SZ')


def transform_to_utc_datetime(dt: datetime) -> datetime:
    return dt.astimezone(tz=timezone.utc)


class DateTimeSpecial(BaseModel):
    datetime_in_utc_with_z_suffix: datetime

    # custom input conversion for that field
    _normalize_datetimes = validator(
        "datetime_in_utc_with_z_suffix",
        allow_reuse=True)(transform_to_utc_datetime)

    class Config:
        json_encoders = {
            # custom output conversion for datetime
            datetime: convert_datetime_to_iso_8601_with_z_suffix
        }


if __name__ == "__main__":
    special_datetime = DateTimeSpecial(datetime_in_utc_with_z_suffix="2042-3-15T12:45+01:00")  # note the different timezone

    # input conversion
    print(special_datetime.datetime_in_utc_with_z_suffix)  # 2042-03-15 11:45:00+00:00

    # output conversion
    print(special_datetime.json())  # {"datetime_in_utc_with_z_suffix": "2042-03-15T11:45:00Z"}

这种变体也适用于 fastapi 的序列化器,在那里我实际上是以这种方式使用它的。


我知道验证器和编码器,但我认为接受哪些格式的定义应该在模式中。不知何故,“日期”类型似乎被假定,并且未在模式“定义”中输出。我们如何将可接受的日期格式添加到模式定义中? - NeilG
@NeilG 通常情况下,如Pydantic的自定义验证器示例所示,您可以在自定义验证器中引发错误。在我的具体情况中(如果我没记错的话),可接受的日期格式是Pydantic的日期时间格式之一。如果您想要另一种日期时间格式,建议您可以尝试使用Pydantic的预验证器,或者确实需要将str作为您的日期时间类。 - Fabian
感谢@Fabian,我很高兴接受默认的Pydantic格式。ISO格式对我来说很好,可能是可接受的输入格式的最佳实践。前端应该处理特定用户格式。但问题在于(默认)发布的模式中没有定义默认格式,我的看法是没有足够的共识可以假设用户知道使用ISO。所以我想现在从你这里听到的是,如果您希望发布的模式在类型定义中包括ISO格式,则必须使用自定义验证器来强制发布它。 - NeilG
说实话,我已经有一段时间没有使用pydantic了,所以我不是100%确定,但如果我记得正确的话,在这种情况下,任何不符合pydantic的日期时间格式定义的格式都会抛出错误。但最好还是自己试一下。 - Fabian
谢谢,@Fabian。错误的格式(在这种情况下是ISO)肯定会出现错误。我想他们不太可能提交一些按照ISO解析的内容而又不知道他们正在使用ISO(你如何将美国日期顺序放入ISO格式而没有意识到呢)。但我这里有很多Windows用户,我不指望他们在基于标准的交换方面非常能干。而且如果有人发送YYYY-DD-MM呢?为了避免感知上的漏洞和支持电话,将所需格式包含在已发布的OpenAPI规范中是有意义的。与浮点数等数据类型不同,日期格式并不那么明显。 - NeilG

19

我认为预验证器可以在这里发挥作用。

from datetime import datetime, date

from pydantic import BaseModel, validator


class OddDate(BaseModel):
    birthdate: date

    @validator("birthdate", pre=True)
    def parse_birthdate(cls, value):
        return datetime.strptime(
            value,
            "%d/%m/%Y"
        ).date()


if __name__ == "__main__":
    odd_date = OddDate(birthdate="12/04/1992")
    print(odd_date.json()) #{"birthdate": "1992-04-12"}

这是我实现验证器的方式。那么如何制作序列化程序呢? - jonsbox
这对我有用,只需确保模型的类型和验证器返回的类型相同并可以通过验证器运行。即参数必须能够接受该类型。 - Zaffer

16

如果您不希望将此行为应用于所有日期时间,可以创建一个扩展datetime的自定义类型。例如,要创建一个自定义类型,始终确保我们拥有一个带有tzinfo设置为UTC的日期时间:

from datetime import datetime, timezone

from pydantic.datetime_parse import parse_datetime


class utc_datetime(datetime):
    @classmethod
    def __get_validators__(cls):
        yield parse_datetime  # default pydantic behavior
        yield cls.ensure_tzinfo

    @classmethod
    def ensure_tzinfo(cls, v):
        # if TZ isn't provided, we assume UTC, but you can do w/e you need
        if v.tzinfo is None:
            return v.replace(tzinfo=timezone.utc)
        # else we convert to utc
        return v.astimezone(timezone.utc)
    
    @staticmethod
    def to_str(dt:datetime) -> str:
        return dt.isoformat() # replace with w/e format you want

那么你的pydantic模型应该如下所示:
from pydantic import BaseModel

class SomeObject(BaseModel):
    some_datetime_in_utc: utc_datetime

    class Config:
        json_encoders = {
            utc_datetime: utc_datetime.to_str
        }

走这条路有助于可重用性和关注点分离 :)

我想,人们可以在对象本身上实现一个自定义的“json”函数来覆盖默认值。但我希望有更好的方法,特别是当调用“dict”时编码器没有被考虑进去。 - aiguofer
2
@user7111260 这就是 Pydantic 配置模型的方式,详见 https://pydantic-docs.helpmanual.io/usage/model_config/。 - bluesmonk
2
为什么那是一个不好的实践? - Phyo Arkar Lwin
1
我不认为这是“不好的做法”,也不是“如此糟糕的做法”。我想人们可能会喜欢将编码器移动到自定义对象中,以便他们不必拥有 Config 类属性,但这是 Pydantic 选择的方式。我曾经考虑过使用一个特殊的方法来移动编码器到对象中,Pydantic 在编码时可能会调用它,但后来我意识到这取决于 JSON 编码器。一个完整的解决方案可能会为 Pydantic BaseModel 提供一种替代的 JSON 编码器,并在那里实现更改。 - NeilG
pydantic v2根据迁移文档已经移除了parse_datetime函数。 - dh762
显示剩余2条评论

5
从pydantic 2.0开始,我们可以使用@field_serializer装饰器进行序列化,以及@field_validator进行验证。
摘自pydantic文档
from datetime import datetime, timezone

from pydantic import BaseModel, field_serializer


class WithCustomEncoders(BaseModel):

    dt: datetime

    @field_serializer('dt')
    def serialize_dt(self, dt: datetime, _info):
        return dt.timestamp()


m = WithCustomEncoders(
    dt=datetime(2032, 6, 1, tzinfo=timezone.utc)
)
print(m.model_dump_json())
#> {"dt":1969660800.0}

而对于验证

from pydantic_core.core_schema import FieldValidationInfo

from pydantic import BaseModel, ValidationError, field_validator


class UserModel(BaseModel):
    name: str
    username: str
    password1: str
    password2: str

    @field_validator('name')
    def name_must_contain_space(cls, v):
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()

    @field_validator('password2')
    def passwords_match(cls, v, info: FieldValidationInfo):
        if 'password1' in info.data and v != info.data['password1']:
            raise ValueError('passwords do not match')
        return v

    @field_validator('username')
    def username_alphanumeric(cls, v):
        assert v.isalnum(), 'must be alphanumeric'
        return v


user = UserModel(
    name='samuel colvin',
    username='scolvin',
    password1='zxcvbn',
    password2='zxcvbn',
)
print(user)
"""
name='Samuel Colvin' username='scolvin' password1='zxcvbn' password2='zxcvbn'
"""

0
为了确保一个日期时间字段是时区感知的并且设置为UTC,我们可以在Pydantic v2中使用带注释的验证器
引用文档中的内容:

当您想要将验证绑定到类型而不是模型或字段时,应该使用带注释的验证器。

from datetime import timezone, datetime
from typing import Annotated

from pydantic import BaseModel, AwareDatetime, AfterValidator, ValidationError


def validate_utc(dt: AwareDatetime) -> AwareDatetime:
    """Validate that the pydantic.AwareDatetime is in UTC."""
    if dt.tzinfo.utcoffset(dt) != timezone.utc.utcoffset(dt):
        raise ValueError("Timezone must be UTC")
    return dt


DatetimeUTC = Annotated[AwareDatetime, AfterValidator(validate_utc)]


class Datapoint(BaseModel):
  timestamp: DatetimeUTC


# valid
d0 = Datapoint(timestamp=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc))
print(f"d0: {d0.timestamp}, timezone: {d0.timestamp.tzinfo}")

# valid
d1 = Datapoint(timestamp='2021-01-01T00:00:00+00:00')
print(f"d1: {d1.timestamp}, timezone: {d1.timestamp.tzinfo}")

# valid
d2 = Datapoint(timestamp='2021-01-01T00:00:00Z')
print(f"d2: {d2.timestamp}, timezone: {d2.timestamp.tzinfo}")

# invalid, missing timezone
try:
    d3 = Datapoint(timestamp='2021-01-01T00:00:00')
except ValidationError as e:
    print(f"d3: {e}")

# invalid, non-UTC timezone
try:
    d4 = Datapoint(timestamp='2021-01-01T00:00:00+02:00')
except ValidationError as e:
    print(f"d4: {e}")

如果我们运行这个程序,我们会看到d0、d1、d2是有效的,而d3和d4则无效。
d0: 2021-01-01 00:00:00+00:00, timezone: UTC

d1: 2021-01-01 00:00:00+00:00, timezone: UTC

d2: 2021-01-01 00:00:00+00:00, timezone: UTC

d3: 1 validation error for Datapoint
timestamp
  Input should have timezone info [type=timezone_aware, input_value='2021-01-01T00:00:00', input_type=str]
    For further information visit https://errors.pydantic.dev/2.3/v/timezone_aware

d4: 1 validation error for Datapoint
timestamp
  Value error, Timezone must be UTC [type=value_error, input_value='2021-01-01T00:00:00+02:00', input_type=str]
    For further information visit https://errors.pydantic.dev/2.3/v/value_error

0
由于pydantic中的validator已被弃用,更好的方法是使用Annotated类,如https://docs.pydantic.dev/latest/concepts/types/#adding-validation-and-serialization中所述:
from typing_extensions import Annotated
from datetime import datetime, timezone
from pydantic import BaseModel, PlainSerializer, BeforeValidator


CustomDatetime = Annotated[
    datetime,
    BeforeValidator(lambda x: datetime.strptime(x, '%Y-%m-%dT%H:%M%z').astimezone(tz=timezone.utc)),
    PlainSerializer(lambda x: x.strftime('%Y-%m-%dT%H:%M:%SZ'))
]


class MyModel(BaseModel):
    datetime_in_utc_with_z_suffix: CustomDatetime


if __name__ == "__main__":
    special_datetime = MyModel(datetime_in_utc_with_z_suffix="2042-3-15T12:45+01:00")  # note the different timezone

    # input conversion
    print(special_datetime.datetime_in_utc_with_z_suffix)  # 2042-03-15 11:45:00+00:00

    # output conversion
    print(special_datetime.model_dump_json())  # {"datetime_in_utc_with_z_suffix": "2042-03-15T11:45:00Z"}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接