diff --git a/pydantic/functional_serializers.py b/pydantic/functional_serializers.py index 849dfe5c43..4e427e83c3 100644 --- a/pydantic/functional_serializers.py +++ b/pydantic/functional_serializers.py @@ -18,6 +18,28 @@ class PlainSerializer: """Plain serializers use a function to modify the output of serialization. + This is particularly helpful when you want to customize the serialization for annotated types. + Consider an input of `list`, which will be serialized into a space-delimited string. + + ```python + from typing import List + + from typing_extensions import Annotated + + from pydantic import BaseModel, PlainSerializer + + CustomStr = Annotated[ + List, PlainSerializer(lambda x: ' '.join(x), return_type=str) + ] + + class StudentModel(BaseModel): + courses: CustomStr + + student = StudentModel(courses=['Math', 'Chemistry', 'English']) + print(student.model_dump()) + #> {'courses': 'Math Chemistry English'} + ``` + Attributes: func: The serializer function. return_type: The return type for the function. If omitted it will be inferred from the type annotation. @@ -61,6 +83,59 @@ class WrapSerializer: """Wrap serializers receive the raw inputs along with a handler function that applies the standard serialization logic, and can modify the resulting value before returning it as the final output of serialization. + For example, here's a scenario in which a wrap serializer transforms timezones to UTC **and** utilizes the existing `datetime` serialization logic. + + ```python + from datetime import datetime, timezone + from typing import Any, Dict + + from typing_extensions import Annotated + + from pydantic import BaseModel, WrapSerializer + + class EventDatetime(BaseModel): + start: datetime + end: datetime + + def convert_to_utc(value: Any, handler, info) -> Dict[str, datetime]: + # Note that `helper` can actually help serialize the `value` for further custom serialization in case it's a subclass. + partial_result = handler(value, info) + if info.mode == 'json': + return { + k: datetime.fromisoformat(v).astimezone(timezone.utc) + for k, v in partial_result.items() + } + return {k: v.astimezone(timezone.utc) for k, v in partial_result.items()} + + UTCEventDatetime = Annotated[EventDatetime, WrapSerializer(convert_to_utc)] + + class EventModel(BaseModel): + event_datetime: UTCEventDatetime + + dt = EventDatetime( + start='2024-01-01T07:00:00-08:00', end='2024-01-03T20:00:00+06:00' + ) + event = EventModel(event_datetime=dt) + print(event.model_dump()) + ''' + { + 'event_datetime': { + 'start': datetime.datetime( + 2024, 1, 1, 15, 0, tzinfo=datetime.timezone.utc + ), + 'end': datetime.datetime( + 2024, 1, 3, 14, 0, tzinfo=datetime.timezone.utc + ), + } + } + ''' + + print(event.model_dump_json()) + ''' + {"event_datetime":{"start":"2024-01-01T15:00:00Z","end":"2024-01-03T14:00:00Z"}} + ''' + ``` + Attributes: func: The serializer function to be wrapped. return_type: The return type for the function. If omitted it will be inferred from the type annotation. @@ -151,6 +226,26 @@ def field_serializer( ) -> Callable[[Any], Any]: """Decorator that enables custom field serialization. + In the below example, a field of type `set` is used to mitigate duplication. A `field_serializer` is used to serialize the data as a sorted list. + + ```python + from typing import Set + + from pydantic import BaseModel, field_serializer + + class StudentModel(BaseModel): + name: str = 'Jane' + courses: Set[str] + + @field_serializer('courses', when_used='json') + def serialize_courses_in_order(courses: Set[str]): + return sorted(courses) + + student = StudentModel(courses={'Math', 'Chemistry', 'English'}) + print(student.model_dump_json()) + #> {"name":"Jane","courses":["Chemistry","English","Math"]} + ``` + See [Custom serializers](../concepts/serialization.md#custom-serializers) for more information. Four signatures are supported: @@ -217,6 +312,30 @@ def model_serializer( ) -> Callable[[Any], Any]: """Decorator that enables custom model serialization. + This is useful when a model need to be serialized in a customized manner, allowing for flexibility beyond just specific fields. + + An example would be to serialize temperature to the same temperature scale, such as degrees Celsius. + + ```python + from typing import Literal + + from pydantic import BaseModel, model_serializer + + class TemperatureModel(BaseModel): + unit: Literal['C', 'F'] + value: int + + @model_serializer() + def serialize_model(self): + if self.unit == 'F': + return {'unit': 'C', 'value': int((self.value - 32) / 1.8)} + return {'unit': self.unit, 'value': self.value} + + temperature = TemperatureModel(unit='F', value=212) + print(temperature.model_dump()) + #> {'unit': 'C', 'value': 100} + ``` + See [Custom serializers](../concepts/serialization.md#custom-serializers) for more information. Args: