Skip to main content

Serialization Control

To customize how data is serialized to JSON or Python objects, you can use function-based serializers or specialized formatting schemas. These allow you to override default behavior for specific types or fields.

Customizing Serialization with Plain Functions

Use plain_serializer_function_ser_schema when you want to completely replace the default serialization logic for a value.

from pydantic_core import SchemaSerializer, core_schema

def double(value: int) -> int:
return value * 2

schema = core_schema.any_schema(
serialization=core_schema.plain_serializer_function_ser_schema(double)
)

serializer = SchemaSerializer(schema)
assert serializer.to_python(4) == 8
assert serializer.to_json(4) == b'8'

The PlainSerializerFunctionSerSchema allows you to specify when_used (defaulting to 'always') and a return_schema to validate the output of your function.

Augmenting Serialization with Wrap Functions

Use wrap_serializer_function_ser_schema when you need to perform logic before or after the standard serialization, or when you need to handle nested items manually.

from collections import deque
from pydantic_core import SchemaSerializer, core_schema, PydanticOmit

def serialize_deque(value, handler, info: core_schema.SerializationInfo):
items = []
for index, item in enumerate(value):
try:
# Use the handler to serialize individual items
v = handler(item, index)
except PydanticOmit:
continue
else:
items.append(v)

if info.mode_is_json():
return items
return deque(items)

schema = core_schema.any_schema(
serialization=core_schema.wrap_serializer_function_ser_schema(
serialize_deque,
info_arg=True,
schema=core_schema.any_schema()
)
)

serializer = SchemaSerializer(schema)
d = deque([1, 2, 3])
assert serializer.to_python(d) == deque([1, 2, 3])
assert serializer.to_json(d) == b'[1,2,3]'

The SerializerFunctionWrapHandler passed as the second argument allows you to invoke the next serializer in the chain.

Accessing Serialization Metadata

You can make serialization decisions based on the current context, mode (JSON vs. Python), or field name by enabling the info_arg and using SerializationInfo or FieldSerializationInfo.

from pydantic_core import core_schema

def custom_serializer(value, info: core_schema.FieldSerializationInfo):
# Access metadata about the current serialization process
if info.mode == 'json':
return f"{info.field_name}: {value}"
return value

schema = core_schema.typed_dict_schema({
'my_field': core_schema.typed_dict_field(
core_schema.int_schema(
serialization=core_schema.plain_serializer_function_ser_schema(
custom_serializer,
info_arg=True,
is_field_serializer=True
)
)
)
})

Key properties available on SerializationInfo:

  • mode: Either 'python' or 'json'.
  • context: The user-provided context dictionary.
  • include/exclude: The sets of fields/indices to include or exclude.
  • by_alias: Whether to use field aliases.
  • field_name: Available only in FieldSerializationInfo when is_field_serializer=True.

Formatting and String Conversion

For simple string-based representations, use format_ser_schema or to_string_ser_schema. These are often used for types like floats, decimals, or network addresses.

from pydantic_core import core_schema

# Format a float to one decimal place for JSON output
float_schema = core_schema.float_schema(
serialization=core_schema.format_ser_schema('0.1f', when_used='unless-none')
)

# Convert a value to a string using str()
str_conv_schema = core_schema.any_schema(
serialization=core_schema.to_string_ser_schema(when_used='json')
)

The when_used parameter (of type WhenUsed) controls when the formatter is applied:

  • 'always': Always use this serializer.
  • 'json': Only use for JSON serialization.
  • 'json-unless-none': Use for JSON unless the value is None.
  • 'unless-none': Use for both Python and JSON unless the value is None.

Filtering Sequences and Dictionaries

You can control which elements of a collection are serialized using filter_seq_schema and filter_dict_schema.

from pydantic_core import core_schema

# Exclude the first two elements of a list
list_schema = core_schema.list_schema(
core_schema.any_schema(),
serialization=core_schema.filter_seq_schema(exclude={0, 1})
)

# Include only specific keys in a dictionary
dict_schema = core_schema.dict_schema(
core_schema.str_schema(),
core_schema.any_schema(),
serialization=core_schema.filter_dict_schema(include={'key1', 'key2'})
)

These schemas use IncExSeqSerSchema and IncExDictSerSchema respectively to manage the inclusion/exclusion logic efficiently during the serialization pass.

Troubleshooting

Return Schema Warnings

If you provide a return_schema in PlainSerializerFunctionSerSchema or WrapSerializerFunctionSerSchema, Pydantic Core will attempt to validate the function's output against it. If validation fails, a UserWarning is issued, but the raw value is still serialized. Ensure your serializer functions return values compatible with their return_schema.

Recursion Errors

Avoid calling SchemaSerializer.to_python or to_json recursively inside a custom serializer function. This can lead to infinite recursion or a PydanticSerializationError. Instead, use the SerializerFunctionWrapHandler provided in wrap serializers to delegate to nested schemas.

Default when_used Behavior

Be aware that different serializers have different default when_used values:

  • Function serializers (function-plain, function-wrap) default to 'always'.
  • Formatting serializers (format, to-string) default to 'json-unless-none'.

If your custom serializer is not being called during to_python(), check if when_used is set to 'json'.