Recursive Schemas and Definitions
To create recursive or self-referencing data structures, you must use a combination of DefinitionsSchema to hold the schema definitions and DefinitionReferenceSchema to point to them. This pattern allows you to define a schema once and reference it multiple times, including within its own definition.
Creating a Recursive Schema
To implement recursion, wrap your main schema in definitions_schema and use definition_reference_schema to create the self-reference.
from pydantic_core import SchemaValidator, core_schema
# Define a recursive "Branch" structure
schema = core_schema.definitions_schema(
# The main entry point schema
core_schema.definition_reference_schema('Branch'),
[
# The actual definition of 'Branch'
core_schema.typed_dict_schema(
{
'name': core_schema.typed_dict_field(core_schema.str_schema()),
'sub_branch': core_schema.typed_dict_field(
core_schema.with_default_schema(
core_schema.nullable_schema(
# Self-reference back to 'Branch'
core_schema.definition_reference_schema('Branch')
),
default=None,
)
),
},
ref='Branch',
)
],
)
v = SchemaValidator(schema)
# Validating a nested structure
data = {'name': 'root', 'sub_branch': {'name': 'child', 'sub_branch': None}}
assert v.validate_python(data) == data
In this example:
definitions_schemaacts as the container for the entire schema logic.- The
definitionslist contains atyped_dict_schemawith a uniqueref='Branch'. definition_reference_schema('Branch')is used both as the entry point and inside the fields to create the recursive loop.
Sharing Common Components
You can also use definitions to avoid duplicating complex schemas that are used in multiple places.
from pydantic_core import SchemaValidator, core_schema
# Define a shared integer schema with specific constraints
shared_int = core_schema.int_schema(ge=0, le=100, ref='percent_int')
schema = core_schema.definitions_schema(
core_schema.typed_dict_schema({
'math_score': core_schema.typed_dict_field(
core_schema.definition_reference_schema('percent_int')
),
'english_score': core_schema.typed_dict_field(
core_schema.definition_reference_schema('percent_int')
),
}),
[shared_int]
)
v = SchemaValidator(schema)
assert v.validate_python({'math_score': 95, 'english_score': 88}) == {'math_score': 95, 'english_score': 88}
Customizing Serialization on References
You can override serialization behavior for a specific reference without changing the underlying definition. This is useful when you want a recursive structure to serialize differently at certain depths.
from pydantic_core import SchemaSerializer, core_schema
schema = core_schema.definitions_schema(
core_schema.definition_reference_schema('Node'),
[
core_schema.typed_dict_schema(
{
'id': core_schema.typed_dict_field(core_schema.int_schema()),
'next': core_schema.typed_dict_field(
core_schema.nullable_schema(
core_schema.definition_reference_schema(
'Node',
# Force string serialization for the 'next' reference
serialization=core_schema.to_string_ser_schema(when_used='always')
)
)
),
},
ref='Node',
)
],
)
s = SchemaSerializer(schema)
data = {'id': 1, 'next': {'id': 2, 'next': None}}
# The 'next' node is serialized as a string representation of the dict
result = s.to_python(data)
assert result['next'] == "{'id': 2, 'next': None}"
Troubleshooting
Missing References
If you use a definition_reference_schema with a schema_ref that does not exist in the definitions list, SchemaValidator will raise a SchemaError during initialization.
from pydantic_core import SchemaValidator, core_schema, SchemaError
import pytest
try:
core_schema.definitions_schema(
core_schema.definition_reference_schema('Missing'),
[core_schema.int_schema(ref='Existing')]
)
except SchemaError:
# Raised because 'Missing' is not in the definitions list
pass
Recursion Loops
Pydantic-core automatically detects cyclic references in your input data to prevent infinite loops. If a cycle is detected during validation, it raises a ValidationError with the type recursion_loop.
from pydantic_core import SchemaValidator, core_schema, ValidationError
# Using the Branch schema from the first example
v = SchemaValidator(...)
cyclic_data = {'name': 'loop'}
cyclic_data['sub_branch'] = cyclic_data # Create a cycle
try:
v.validate_python(cyclic_data)
except ValidationError as e:
assert e.errors()[0]['type'] == 'recursion_loop'
During serialization, detecting a circular reference will result in a ValueError with the message "Circular reference detected".