Skip to main content

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:

  1. smart (Default): This mode attempts to find the "best" match rather than just the first one. It prefers exact matches (e.g., an int for an int_schema) over matches that require coercion (e.g., a str "1" for an int_schema). If multiple complex types (like TypedDict) match, it prefers the one that matches the most fields.
  2. left_to_right: This mode is simpler and faster. It iterates through the choices and 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_invalid if no choices match, or a collection of errors from the attempted schemas.
  • Tagged Union: Raises union_tag_not_found if the discriminator field is missing, or union_tag_invalid if the tag value does not exist in the choices dictionary.

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.