Unions and Polymorphism
Unions allow you to define schemas that accept data matching one of several possible types. In pydantic-core, this is implemented through two primary structures: UnionSchema for general-purpose matching and TaggedUnionSchema for high-performance polymorphic models.
Both are defined in pydantic_core.core_schema and are typically instantiated using the union_schema() and tagged_union_schema() helper functions.
Standard Unions
A standard union attempts to validate the input against a list of candidate schemas. It is defined by the UnionSchema class and created via union_schema().
Matching Modes
The UnionSchema supports two matching modes via the mode field:
smart(Default): This mode attempts to find the "best" match rather than just the first one. It prefers exact matches (e.g., anintfor anint_schema) over matches that require coercion (e.g., astr"1" for anint_schema). If multiple complex types (likeTypedDict) match, it prefers the one that matches the most fields.left_to_right: This mode is simpler and faster. It iterates through thechoicesand returns the result of the first schema that successfully validates the input.
from pydantic_core import SchemaValidator, core_schema
# A simple union accepting either a string or an integer
schema = core_schema.union_schema([
core_schema.str_schema(),
core_schema.int_schema()
])
v = SchemaValidator(schema)
assert v.validate_python('hello') == 'hello'
assert v.validate_python(1) == 1
Auto-Collapse
By default, UnionSchema has auto_collapse=True. If a union contains only a single choice, pydantic-core will automatically collapse the union and use the inner validator directly, reducing overhead.
Tagged Unions (Polymorphism)
TaggedUnionSchema is designed for polymorphic data where a specific field (the "discriminator" or "tag") identifies which schema should be used. This is significantly more performant than a standard union because it avoids trial-and-error validation; it looks up the correct schema in a dict based on the tag.
Using a Field Discriminator
The most common use case is a string discriminator that matches a key in the input data.
from pydantic_core import SchemaValidator, core_schema
apple_schema = core_schema.typed_dict_schema({
'type': core_schema.typed_dict_field(core_schema.str_schema()),
'radius': core_schema.typed_dict_field(core_schema.int_schema()),
})
banana_schema = core_schema.typed_dict_schema({
'type': core_schema.typed_dict_field(core_schema.str_schema()),
'length': core_schema.typed_dict_field(core_schema.int_schema()),
})
# The 'type' field determines which schema to use
schema = core_schema.tagged_union_schema(
choices={
'apple': apple_schema,
'banana': banana_schema,
},
discriminator='type',
)
v = SchemaValidator(schema)
assert v.validate_python({'type': 'apple', 'radius': 10}) == {'type': 'apple', 'radius': 10}
Complex Discriminators
The discriminator in TaggedUnionSchema is highly flexible and can be:
- A string: The name of the field to look up.
- A list of strings/ints: A path to the discriminator value in nested data (e.g.,
['metadata', 'kind']). - A list of lists: Multiple paths to try in order.
- A Callable: A function that takes the input data and returns the tag.
Example of a path-based discriminator from pydantic-core/tests/validators/test_tagged_union.py:
# Matches 'food' key OR index 1 of the 'menu' list
schema = core_schema.tagged_union_schema(
choices={
'apple': apple_schema,
'banana': banana_schema,
},
discriminator=[['food'], ['menu', 1]],
)
v = SchemaValidator(schema)
assert v.validate_python({'food': 'apple', 'radius': 5})['radius'] == 5
assert v.validate_python({'menu': ['item', 'banana'], 'length': 10})['length'] == 10
Error Handling
Unions provide specific error types when validation fails:
- Standard Union: Typically returns a
union_tag_invalidif no choices match, or a collection of errors from the attempted schemas. - Tagged Union: Raises
union_tag_not_foundif the discriminator field is missing, orunion_tag_invalidif the tag value does not exist in thechoicesdictionary.
You can customize these errors using custom_error_type, custom_error_message, and custom_error_context within the schema definition.
Advanced Configuration
From Attributes
The from_attributes flag (defaulting to True in TaggedUnionSchema) determines whether the validator should attempt to retrieve the discriminator value from object attributes if the input is not a dictionary. This is essential when validating class instances or ORM models.
Strict Mode
When strict=True is set on a UnionSchema, the matching logic becomes more rigid. In smart mode, this prevents the validator from attempting coercions that might otherwise be considered "good enough" matches, ensuring that only exact type matches are accepted.