Discriminated and Tagged Unions
To implement high-performance unions in this project, use a discriminator field to determine which sub-schema to validate against. This approach, implemented via TaggedUnionSchema, is significantly faster than standard unions because it avoids attempting to validate against every possible choice.
Implement a Tagged Union with a String Discriminator
The most common way to implement a discriminated union is by using a single field name as the discriminator.
from pydantic_core import SchemaValidator, core_schema
# Define sub-schemas
apple_schema = core_schema.typed_dict_schema(
{
'type': core_schema.typed_dict_field(core_schema.str_schema()),
'bar': 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()),
'spam': core_schema.typed_dict_field(
core_schema.list_schema(items_schema=core_schema.int_schema())
),
}
)
# Create the tagged union schema
schema = core_schema.tagged_union_schema(
choices={
'apple': apple_schema,
'banana': banana_schema,
},
discriminator='type',
)
v = SchemaValidator(schema)
# Validation uses the 'type' field to jump directly to the correct schema
assert v.validate_python({'type': 'apple', 'bar': '123'}) == {'type': 'apple', 'bar': 123}
Key Components
choices: A dictionary mapping discriminator values (tags) to their correspondingCoreSchema.discriminator: The field name (or path) used to look up the tag in the input data.from_attributes: Defaults toTrue. IfTrue, the validator will attempt to extract the discriminator from object attributes if the input is not a dictionary.
Use Standard Unions for Simple Types
If you do not have a discriminator field, use UnionSchema via the union_schema() helper. Note that this is less performant for large sets of choices as it may try multiple validators.
from pydantic_core import SchemaValidator, core_schema
schema = core_schema.union_schema(
choices=[
core_schema.str_schema(),
core_schema.int_schema()
],
mode='smart'
)
v = SchemaValidator(schema)
assert v.validate_python('hello') == 'hello'
assert v.validate_python(1) == 1
Union Modes
smart(default): Attempts to find the "best" match among the choices.left_to_right: Returns the first choice that succeeds validation.
Advanced Discriminator Lookups
The discriminator argument in tagged_union_schema supports complex lookup logic, including nested paths and fallback options.
Nested and Fallback Paths
You can provide a list of paths. Each path is a list of strings or integers representing keys or indices. The validator will try each path in order until it finds a value.
from pydantic_core import SchemaValidator, core_schema
schema = core_schema.tagged_union_schema(
choices={
'apple': apple_schema,
'banana': banana_schema,
},
# Try 'food' key first, then try index 1 of the 'menu' list
discriminator=[['food'], ['menu', 1]],
)
v = SchemaValidator(schema)
# Matches via 'food'
assert v.validate_python({'food': 'apple', 'type': 'apple', 'bar': 1}) == {'type': 'apple', 'bar': 1}
# Matches via 'menu' index 1
assert v.validate_python({'menu': ['item', 'banana'], 'type': 'banana', 'spam': [1]}) == {'type': 'banana', 'spam': [1]}
Callable Discriminators
For logic that cannot be expressed as a simple path, you can provide a Python callable.
from pydantic_core import SchemaValidator, core_schema
def custom_discriminator(obj):
if isinstance(obj, dict) and 'kind' in obj:
return obj['kind']
return 'default'
schema = core_schema.tagged_union_schema(
choices={
'a': core_schema.str_schema(),
'default': core_schema.int_schema(),
},
discriminator=custom_discriminator,
)
v = SchemaValidator(schema)
assert v.validate_python({'kind': 'a'}) == 'a'
Using Enums as Tags
You can use Enum members as keys in the choices dictionary. The validator will match the input against the enum value or the enum member itself.
from enum import Enum
from pydantic_core import SchemaValidator, core_schema
class Kind(str, Enum):
APPLE = 'apple'
BANANA = 'banana'
schema = core_schema.tagged_union_schema(
discriminator='type',
choices={
Kind.APPLE: apple_schema,
Kind.BANANA: banana_schema,
}
)
v = SchemaValidator(schema)
assert v.validate_python({'type': 'apple', 'bar': 1})['type'] == 'apple'
Troubleshooting
Input Must Be a Dictionary
By default, TaggedUnionSchema expects a dictionary to extract the discriminator. If your input is a custom object, ensure from_attributes is set to True (which is the default in tagged_union_schema).
Recursive Lookups
If a choice in TaggedUnionSchema is a string, it is treated as a reference to another key within the choices map. This is used internally to handle recursive schemas without duplicating the underlying Rust validators.
Missing Tags
If the discriminator field is missing or the value found does not match any key in choices, a ValidationError with type union_tag_not_found or union_tag_invalid will be raised. You can customize these errors using custom_error_type and custom_error_message in the tagged_union_schema call.