Skip to main content

Serialization of Primitive Types

In pydantic-core, serialization of primitive types is managed through specialized serialization schemas. These schemas allow you to control how scalar values (like integers, floats, and strings) or complex objects are transformed when converted to Python objects or JSON strings.

The core serialization schemas for primitives are defined in pydantic_core/core_schema.py and include SimpleSerSchema, ToStringSerSchema, and FormatSerSchema.

Simple Serialization Overrides

The SimpleSerSchema is used to specify a standard serialization type for a field, overriding the default behavior of the core schema it is attached to.

from pydantic_core import core_schema

# Forcing a field to serialize as a string
schema = core_schema.int_schema(
serialization=core_schema.simple_ser_schema('str')
)

Runtime Type Inference with 'any'

A powerful use case for SimpleSerSchema is using the 'any' type. This instructs the Rust-based serializer to perform runtime type discovery rather than relying on the static type defined in the schema. This is frequently used in pydantic to handle cases where a value might be a subclass or a type that requires the serializer to inspect the object at runtime.

For example, in pydantic/_internal/_generate_schema.py, simple_ser_schema('any') is used to ensure that complex types or types with custom validators are serialized correctly by the underlying engine.

String Conversion

The ToStringSerSchema (created via to_string_ser_schema) forces the serializer to use the Python str() or __str__ method. This is the standard way to ensure that objects which are not natively supported by JSON (like UUIDs, IP addresses, or custom classes) are represented as strings.

Implementation in Pydantic Types

Many specialized types in Pydantic use this to ensure consistent JSON output. For instance, in pydantic/color.py, the Color class uses it to ensure the color is serialized using its __str__ representation:

# From pydantic/color.py
return core_schema.with_info_plain_validator_function(
cls._validate,
serialization=core_schema.to_string_ser_schema()
)

Similarly, pydantic/networks.py uses it for IP addresses and URLs to ensure they appear as strings in the final JSON output.

Formatted Serialization

The FormatSerSchema (created via format_ser_schema) leverages Python's built-in format() function. This allows for precise control over numeric precision, date formatting, and string padding.

Usage Examples

As demonstrated in pydantic-core/tests/serializers/test_format.py, you can use this for various formatting requirements:

from pydantic_core import SchemaSerializer, core_schema

# Float precision: 42.12345 -> "42.1235"
s = SchemaSerializer(
core_schema.float_schema(
serialization=core_schema.format_ser_schema('0.4f')
)
)

# Date formatting: date(2022, 11, 20) -> "2022-11-20"
s = SchemaSerializer(
core_schema.date_schema(
serialization=core_schema.format_ser_schema('%Y-%m-%d')
)
)

# String padding: "foo" -> " foo "
s = SchemaSerializer(
core_schema.str_schema(
serialization=core_schema.format_ser_schema('^5s')
)
)

Conditional Serialization with when_used

All primitive serialization schemas support the when_used parameter, which determines the context in which the transformation is applied. This is defined by the WhenUsed type alias in pydantic_core/core_schema.py:

  • 'always': The transformation is applied in both to_python() and to_json() calls.
  • 'unless-none': Applied in both modes, but skipped if the value is None.
  • 'json': Applied only when serializing to JSON (e.g., to_json() or to_python(mode='json')).
  • 'json-unless-none': The default for ToStringSerSchema and FormatSerSchema. Applied only for JSON serialization and only if the value is not None.

Default Behavior

The default json-unless-none is chosen because it allows Python-to-Python serialization to maintain rich objects (like datetime or UUID) while ensuring the output is valid JSON when needed.

# Default behavior (json-unless-none)
s = SchemaSerializer(
core_schema.any_schema(serialization=core_schema.to_string_ser_schema())
)

# Returns the original int object
assert s.to_python(123) == 123

# Returns a string for JSON compatibility
assert s.to_python(123, mode='json') == '123'

Error Handling

When using FormatSerSchema, the formatting_string must be compatible with the type being serialized. If the Python format() call fails (e.g., trying to use a decimal format 'd' on a string), pydantic-core raises a PydanticSerializationError.

# This will raise PydanticSerializationError if a string is passed
s = SchemaSerializer(
core_schema.any_schema(serialization=core_schema.format_ser_schema('^5d'))
)

This error encapsulates the underlying Python exception (like ValueError or TypeError) that occurred during the formatting process.