JSON Serialization Configuration
JSON serialization in this codebase is highly configurable, allowing you to control how complex Python types are represented in JSON output. This is achieved through two primary mechanisms: global configuration via CoreConfig and granular, per-field customization using specialized serialization schemas like FormatSerSchema and ToStringSerSchema.
Global Configuration
The CoreConfig class in pydantic_core.core_schema defines several flags that dictate the default JSON serialization behavior for common complex types across an entire schema.
Temporal Types
For datetime, date, time, and timedelta objects, you can control the precision and format:
ser_json_temporal: The primary setting for all temporal types.'iso8601'(default): Serializes to ISO 8601 strings.'seconds': Serializes to a float representing total seconds.'milliseconds': Serializes to an integer representing total milliseconds.
ser_json_timedelta: Specifically fortimedeltavalues.'iso8601'(default): Serializes to ISO 8601 duration strings (e.g.,P1DT2H).'float': Serializes to a float representing total seconds.
Precedence Note: If ser_json_temporal is explicitly set, it takes precedence over ser_json_timedelta for timedelta values.
Bytes and Binary Data
The ser_json_bytes setting determines how bytes and bytearray objects are encoded:
'utf8'(default): Attempts to decode bytes as UTF-8. This will fail with aPydanticSerializationErrorif the data is not valid UTF-8.'base64': Encodes bytes as a Base64 string.'hex': Encodes bytes as a hexadecimal string.
Infinity and NaN
Standard JSON does not support Infinity or NaN. The ser_json_inf_nan setting handles these float values:
'null'(default): Serializesinfandnantonull.'strings': Serializes to"Infinity","-Infinity", or"NaN".'constants': Serializes to literalInfinityandNaN. Warning: This produces non-standard JSON that many parsers cannot handle.
from pydantic_core import SchemaSerializer, core_schema
# Example: Global config for Base64 bytes and string Inf/NaN
config = core_schema.CoreConfig(
ser_json_bytes='base64',
ser_json_inf_nan='strings'
)
s = SchemaSerializer(core_schema.any_schema(), config)
assert s.to_json(b'hello') == b'"aGVsbG8="'
assert s.to_json(float('inf')) == b'"Infinity"'
Custom Serialization Schemas
When global settings are insufficient, you can attach specific serialization schemas to individual fields or types within your CoreSchema.
Formatting with FormatSerSchema
The FormatSerSchema allows you to use Python's standard format() syntax to represent values as strings in JSON. This is commonly used for controlling decimal precision or date formatting.
# Custom float precision in JSON
s = SchemaSerializer(
core_schema.float_schema(
serialization=core_schema.format_ser_schema('0.4f')
)
)
assert s.to_json(42.123456) == b'"42.1235"'
String Conversion with ToStringSerSchema
The ToStringSerSchema simply calls str() on the value. This is useful for types like UUIDs, URLs, or custom objects that have a meaningful string representation but no native JSON equivalent.
# Force integer to string in JSON
s = SchemaSerializer(
core_schema.int_schema(
serialization=core_schema.to_string_ser_schema()
)
)
assert s.to_json(123) == b'"123"'
Type Forcing with SimpleSerSchema
The SimpleSerSchema forces a value to be treated as a specific core type during serialization. For example, you can force a complex object to be serialized as a simple dictionary or string.
Conditional Serialization
The when_used parameter (of type WhenUsed) is available on most custom serialization schemas. It controls exactly when the custom logic is applied:
| Value | Description |
|---|---|
'always' | Always use the custom serializer. |
'unless-none' | Use the custom serializer unless the value is None. |
'json' | Only use the custom serializer when exporting to JSON. |
'json-unless-none' | (Default for formatters) Only use for JSON and only if not None. |
Handling None Values
A common pitfall occurs when using FormatSerSchema with when_used='json'. If the value is None, the serializer will attempt format(None, '...'), which raises a TypeError. To avoid this, use the unless-none variants:
# Safe handling of None with formatters
schema = core_schema.any_schema(
serialization=core_schema.format_ser_schema('0.1f', when_used='json-unless-none')
)
s = SchemaSerializer(schema)
assert s.to_json(1.23) == b'"1.2"'
assert s.to_json(None) == b'null' # Does not crash
Implementation Details
The serialization logic is implemented in the Rust core for performance. When to_json() is called, the SchemaSerializer traverses the data according to the CoreSchema. If a serialization key is present (containing a FormatSerSchema, ToStringSerSchema, etc.), the core switches from its default internal serialization logic to the specified custom behavior.
For global configurations, the CoreConfig values are passed into the SchemaSerializer at instantiation and cached in the internal Config struct in Rust, ensuring that every serialization operation respects these settings without repeated lookups.