Skip to main content

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 corresponding CoreSchema.
  • discriminator: The field name (or path) used to look up the tag in the input data.
  • from_attributes: Defaults to True. If True, 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.