Polymorphic Types with Unions
Polymorphic types in pydantic-core allow a single field or value to conform to one of several different schemas. This is implemented through two primary mechanisms: standard unions (UnionSchema) and performance-optimized tagged unions (TaggedUnionSchema).
Standard Unions
The UnionSchema (defined in pydantic_core/core_schema.py) is used when you have a list of potential schemas and want to validate an input against them. It is typically created using the core_schema.union_schema() helper.
Validation Modes
A key feature of UnionSchema is the mode parameter, which determines how pydantic-core selects the matching schema from the choices list.
- Smart Mode (
mode='smart'): This is the default. The validator attempts to find the "best" match by trying to validate the input against each choice. It prioritizes matches that don't require coercion or that are "closer" to the input type. - Left-to-Right Mode (
mode='left_to_right'): The validator tries each schema in the order they appear in thechoiceslist and returns the first one that succeeds. This is useful when the order of preference is explicit.
As shown in pydantic-core/tests/validators/test_union.py, the mode significantly impacts the result when types overlap:
from pydantic_core import SchemaValidator, core_schema
choices = [core_schema.int_schema(), core_schema.float_schema()]
# Smart union prefers the exact type match
v_smart = SchemaValidator(core_schema.union_schema(choices, mode='smart'))
assert isinstance(v_smart.validate_python(1.0), float)
# Left-to-right union selects the first successful validation
v_ltr = SchemaValidator(core_schema.union_schema(choices, mode='left_to_right'))
# 1.0 can be validated as an int (1), so int is chosen
assert isinstance(v_ltr.validate_python(1.0), int)
Tagged Unions
TaggedUnionSchema (also in pydantic_core/core_schema.py) provides a more efficient way to handle polymorphism by using a "discriminator" to determine which schema to use. Instead of trying every choice, it looks up the correct schema in a choices dictionary using a tag extracted from the input.
The Discriminator
The discriminator defines how to extract the tag from the input data. In this codebase, it can take several forms:
- Field Name: A string representing a key in a dictionary or an attribute on an object.
- Path: A list of strings or integers representing a nested path (e.g.,
['metadata', 'type']). - Fallback Paths: A list of lists, where the validator tries each path until it finds a value.
- Callable: A custom function that takes the input and returns the tag.
Example of a path-based discriminator from pydantic-core/tests/validators/test_tagged_union.py:
schema = core_schema.tagged_union_schema(
choices={
'apple': apple_schema,
'banana': banana_schema,
},
# Try 'food' key first, then 'menu' index 1
discriminator=[['food'], ['menu', 1]],
)
Choice Mapping and Optimization
The choices parameter is a dictionary mapping tags (like 'apple' or 'banana') to CoreSchema objects.
An optimization supported by TaggedUnionSchema allows mapping a tag to another string instead of a full schema. This is used to point multiple tags to the same schema definition without duplicating the schema structure in the underlying Rust implementation.
schema = core_schema.tagged_union_schema(
choices={
'apple': apple_schema,
'fruit': 'apple', # Points to the 'apple' schema
},
discriminator='type',
)
Advanced Configuration
Object Validation with from_attributes
The TaggedUnionSchema includes a from_attributes boolean (defaulting to True). When enabled, the discriminator can extract tags from object attributes as well as dictionary keys. This is essential for validating class instances or ORM models where the discriminator field is an attribute.
Error Handling
When validation fails in a tagged union, pydantic-core raises specific errors:
union_tag_not_found: Raised when the discriminator field or path is missing from the input.union_tag_invalid: Raised when the tag is found but does not match any key in thechoicesdictionary.
These errors provide context, such as the expected tags and the discriminator logic used, as seen in the union_tag_not_found error context in test_tagged_union.py:
"Unable to extract tag using discriminator 'food' | 'menu'.1"
Serialization
Unions also affect how data is serialized back to Python or JSON. The UnionSchema can include a serialization field of type SerSchema. During serialization, pydantic-core uses the same logic to determine which sub-schema's serializer to apply, ensuring that the polymorphic structure is preserved in the output.