Three fundamental union validation modes
Pydantic supports three fundamental approaches to validating unions: left to right mode, smart mode, and discriminated unions. Left to right mode tries each member in order and returns the first match. Smart mode is the default in Pydantic >=2 and attempts to find the best match using metrics like number of valid fields set and exactness of the match. Discriminated unions use a discriminator to select which member to validate against, offering better performance and predictability.
Left to right union mode behavior
In left to right mode, validation is attempted against each member of the union in the order they are defined, and the first successful validation is accepted as input. If validation fails on all members, the validation error includes errors from all members. This mode must be set as a Field parameter using union_mode='left_to_right' and is not the default in Pydantic >=2 because it often leads to unexpected validation results.
Smart mode default union validation
In Pydantic >=2, union_mode='smart' is the default mode for union validation. Smart mode attempts to select the best match for the input from union members using two metrics: the number of valid fields set (relevant for models, dataclasses, and typed dicts, introduced in Pydantic v2.8.0) and exactness of the match (relevant for all types). The exact algorithm may change between Pydantic minor releases.
Exactness scoring in smart mode unions
In smart mode, exactness scoring for union member matches uses three categories from highest to lowest score: an exact type match (e.g., int input to a float | int union), validation that would have succeeded in strict mode, and validation that would have succeeded in lax mode. The union member with the highest exactness score is considered the best match.
Smart mode algorithm for BaseModel, dataclass, and TypedDict
For BaseModel, dataclass, and TypedDict union members in smart mode: union members are attempted left to right with successful matches scored into exactness categories and valid fields counted; after all members are evaluated, the member with the highest valid fields set count is returned; if there is a tie in valid fields count, exactness score is used as a tiebreaker; if validation failed on all members, all errors are returned.
Smart mode algorithm for other data types
For all other data types in smart mode union validation: union members are attempted left to right with successful matches scored into exactness categories; if validation succeeds with an exact type match, that member is returned immediately without attempting following members; if validation succeeded on at least one member as a strict match, the leftmost strict match is returned; if validation succeeded on at least one member in lax mode, the leftmost match is returned; if validation failed on all members, all errors are returned.
Discriminated unions with string discriminators
Discriminated unions can be validated efficiently by specifying a common field on union members that distinguishes which case the data should be validated against. To use this, define a common field on each model (e.g., pet_type) typed as accepting one or multiple literal values, then set the discriminator parameter of Field() or use the Discriminator type. This makes validation more efficient and avoids proliferation of errors when validation fails.
Callable discriminator for unions
When there is no single uniform field across all union members, a callable Discriminator can be used. The callable discriminator function must handle both dict and model type inputs, similar to mode='before' validators, because Pydantic uses callable discriminators for both validation and serialization. The function should return the discriminator value or None if not found.
Left to right mode with unexpected type coercion
When using union_mode='left_to_right' with int | str, validation of a numeric string like '456' will match the int member first in lax mode because the numeric string is valid as input to int. This results in the value being coerced to int instead of remaining a string, which is often unexpected. The order of union members is critical with this mode.
Discriminated unions with custom error handling
The Discriminator constructor accepts custom_error_type, custom_error_message, and custom_error_context parameters to customize error messages when validation fails. custom_error_type sets the type attribute of the ValidationError, custom_error_message sets the msg attribute, and custom_error_context sets the ctx attribute.
Nested discriminated unions
Multiple discriminators can be combined by creating nested Annotated types. Only one discriminator can be set for a field directly, but by nesting Annotated types with their own discriminators, you can validate complex union structures with multiple levels of discrimination.
Tag annotation for clearer union error messages
Union cases can be labeled with Tag annotations to make error messages clearer. When using Tag, the error output includes the tag label instead of showing the full complex type representation, making it easier to understand which union case failed.
Root models with Literal in discriminated unions
As of Pydantic v2.13, Root models with a Literal root type can be used in place of Literal types in discriminated unions.
Left to right union mode example
Example showing left to right union mode:
from pydantic import BaseModel, Field, ValidationError
class User(BaseModel):
id: str | int = Field(union_mode='left_to_right')
print(User(id=123))
#> id=123
print(User(id='hello'))
#> id='hello'
try:
User(id=[])
except ValidationError as e:
print(e)
This demonstrates that with left to right mode, the first matching member (str in this case) is tried first, then int.
Smart mode union example with multiple types
Example showing smart mode union validation:
from uuid import UUID
from pydantic import BaseModel
class User(BaseModel):
id: int | str | UUID
name: str
user_01 = User(id=123, name='John Doe')
print(user_01)
#> id=123 name='John Doe'
user_02 = User(id='1234', name='John Doe')
print(user_02)
#> id='1234' name='John Doe'
user_03_uuid = UUID('cf57432e-809e-4353-adbd-9d5c0d733868')
user_03 = User(id=user_03_uuid, name='John Doe')
print(user_03)
#> id=UUID('cf57432e-809e-4353-adbd-9d5c0d733868') name='John Doe'
Smart mode correctly identifies the best match for each input type.
Discriminated union with string discriminator example
Example showing discriminated unions with string discriminators:
from typing import Literal
from pydantic import BaseModel, Field, ValidationError
class Cat(BaseModel):
pet_type: Literal['cat']
meows: int
class Dog(BaseModel):
pet_type: Literal['dog']
barks: float
class Lizard(BaseModel):
pet_type: Literal['reptile', 'lizard']
scales: bool
class Model(BaseModel):
pet: Cat | Dog | Lizard = Field(discriminator='pet_type')
n: int
print(Model(pet={'pet_type': 'dog', 'barks': 3.14}, n=1))
#> pet=Dog(pet_type='dog', barks=3.14) n=1
This shows how the discriminator field is used to select the correct union member.
Callable discriminator example
Example showing callable Discriminator for unions:
from typing import Annotated, Any, Literal
from pydantic import BaseModel, Discriminator, Tag
class Pie(BaseModel):
time_to_cook: int
num_ingredients: int
class ApplePie(Pie):
fruit: Literal['apple'] = 'apple'
class PumpkinPie(Pie):
filling: Literal['pumpkin'] = 'pumpkin'
def get_discriminator_value(v: Any) -> str | None:
if isinstance(v, dict):
return v.get('fruit', v.get('filling'))
return getattr(v, 'fruit', getattr(v, 'filling', None))
class ThanksgivingDinner(BaseModel):
dessert: Annotated[
Annotated[ApplePie, Tag('apple')] | Annotated[PumpkinPie, Tag('pumpkin')],
Discriminator(get_discriminator_value),
]
apple_variation = ThanksgivingDinner.model_validate(
{'dessert': {'fruit': 'apple', 'time_to_cook': 60, 'num_ingredients': 8}}
)
print(repr(apple_variation))
#> ThanksgivingDinner(dessert=ApplePie(time_to_cook=60, num_ingredients=8, fruit='apple'))
This shows how a callable discriminator handles both dict and model type inputs.
Nested discriminated unions example
Example showing nested discriminated unions:
from typing import Annotated, Literal
from pydantic import BaseModel, Field, ValidationError
class BlackCat(BaseModel):
pet_type: Literal['cat']
color: Literal['black']
black_name: str
class WhiteCat(BaseModel):
pet_type: Literal['cat']
color: Literal['white']
white_name: str
Cat = Annotated[BlackCat | WhiteCat, Field(discriminator='color')]
class Dog(BaseModel):
pet_type: Literal['dog']
name: str
Pet = Annotated[Cat | Dog, Field(discriminator='pet_type')]
class Model(BaseModel):
pet: Pet
n: int
m = Model(pet={'pet_type': 'cat', 'color': 'black', 'black_name': 'felix'}, n=1)
print(m)
#> pet=BlackCat(pet_type='cat', color='black', black_name='felix') n=1
This demonstrates combining multiple discriminators through nested Annotated types.
Callable discriminator with primitive types
Example showing callable Discriminator with union of models and primitive types:
from typing import Annotated, Any
from pydantic import BaseModel, Discriminator, Tag, ValidationError
def model_x_discriminator(v: Any) -> str | None:
if isinstance(v, int):
return 'int'
if isinstance(v, (dict, BaseModel)):
return 'model'
else:
return None
class SpecialValue(BaseModel):
value: int
class DiscriminatedModel(BaseModel):
value: Annotated[
Annotated[int, Tag('int')] | Annotated['SpecialValue', Tag('model')],
Discriminator(model_x_discriminator),
]
model_data = {'value': {'value': 1}}
m = DiscriminatedModel.model_validate(model_data)
print(m)
#> value=SpecialValue(value=1)
int_data = {'value': 123}
m = DiscriminatedModel.model_validate(int_data)
print(m)
#> value=123
This shows how callable discriminators can handle unions of both model and primitive types.
Recommendation for union validation modes
Pydantic documentation recommends using discriminated unions in general, as they are more performant and more predictable than untagged unions. For complex cases with untagged unions, it is recommended to use union_mode='left_to_right' if you need guarantees about the order of validation attempts. For incredibly specialized behavior, a custom validator can be used.