Skip to main content

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_schema acts as the container for the entire schema logic.
  • The definitions list contains a typed_dict_schema with a unique ref='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".