Reputation: 582
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
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
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
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
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
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
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
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