jonsbox
jonsbox

Reputation: 582

How to change date format in pydantic

How to change date format in pydantic for validation and serialization? For validation I am using @validator. Is there an solution for both cases?

Upvotes: 33

Views: 97959

Answers (7)

focus zheng
focus zheng

Reputation: 380

https://docs.pydantic.dev/1.10/usage/exporting_models/#json_encoders

from datetime import datetime, timedelta
from pydantic import BaseModel
from pydantic.json import timedelta_isoformat


class WithCustomEncoders(BaseModel):
    dt: datetime
    class Config:
        json_encoders = {
            datetime: lambda v: v.strftime("%Y%m%d%H%M%S"),
        }


m = WithCustomEncoders(dt=datetime.now())
print(m.json())
#> '{"dt": "20240506153418"}'

Upvotes: 0

Takeshi Gitano
Takeshi Gitano

Reputation: 849

Since validator is deprecated in pydantic, a better approach should be to use Annotated class, as stated in 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"}

Upvotes: 3

dh762
dh762

Reputation: 2428

To make sure that a datetime field is Timezone-Aware and set to UTC we can use Annotated validators in Pydantic v2.

To quote from the docs:

You should use Annotated validators whenever you want to bind validation to a type instead of model or field.

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}")

If we run this we see d0, d1, d2 are valid while d3 and d4 are not:

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

Upvotes: 2

Trapsilo Bumi
Trapsilo Bumi

Reputation: 1329

As of pydantic 2.0, we can use the @field_serializer decorator for serialization, and @field_validator for validation.

Taken from pydantic docs:

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}

And for validation:

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'
"""

Upvotes: 16

aiguofer
aiguofer

Reputation: 2135

In case you don't necessarily want to apply this behavior to all datetimes, you can create a custom type extending datetime. For example, to make a custom type that always ensures we have a datetime with tzinfo set to 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

Then your pydantic models would look like:

from pydantic import BaseModel

class SomeObject(BaseModel):
    some_datetime_in_utc: utc_datetime

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

Going this route helps with reusability and separation of concerns :)

Upvotes: 16

Fabian
Fabian

Reputation: 493

You can implement a custom json serializer by using pydantic's custom json encoders. Then, together with pydantic's custom validator, you can have both functionalities.


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"}

This variant also works in fastapi's serializer where I am actually using it in that way.

Upvotes: 32

Omer Shacham
Omer Shacham

Reputation: 668

I think that pre validator can help here.

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"}

Upvotes: 21

Related Questions