Unions
The Union
type allows a model attribute to accept different types, e.g.:
from typing import Union
from uuid import UUID
from pydantic import BaseModel
class User(BaseModel):
id: Union[int, str, UUID]
name: str
user_01 = User(id=123, name='John Doe')
print(user_01)
#> id=123 name='John Doe'
print(user_01.id)
#> 123
user_02 = User(id='1234', name='John Doe')
print(user_02)
#> id='1234' name='John Doe'
print(user_02.id)
#> 1234
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'
print(user_03.id)
#> cf57432e-809e-4353-adbd-9d5c0d733868
print(user_03_uuid.int)
#> 275603287559914445491632874575877060712
Tip
The type Optional[x]
is a shorthand for Union[x, None]
.
Optional[x]
can also be used to specify a required field that can take None
as a value.
See more details in Required fields.
Union Mode¶
By default Union
validation will try to return the variant which is the best match for the input.
Consider for example the case of Union[int, str]
. When strict
mode is not enabled
then int
fields will accept str
inputs. In the example below, the id
field (which is Union[int, str]
)
will accept the string '123'
as an input, and preserve it as a string:
from typing import Union
from pydantic import BaseModel
class User(BaseModel):
id: Union[int, str]
age: int
print(User(id='123', age='45'))
#> id='123' age=45
print(type(User(id='123', age='45').id))
#> <class 'str'>
This is known as 'smart'
mode for Union
validation.
At present only one other Union
validation mode exists, called 'left_to_right'
validation. In this mode
variants are attempted from left to right and the first successful validation is accepted as input.
Consider the same example, this time with union_mode='left_to_right'
set as a Field
parameter on id
. With this validation mode, the int
variant will coerce strings of digits into int
values:
from typing import Union
from pydantic import BaseModel, Field
class User(BaseModel):
id: Union[int, str] = Field(..., union_mode='left_to_right')
age: int
print(User(id='123', age='45'))
#> id=123 age=45
print(type(User(id='123', age='45').id))
#> <class 'int'>
Discriminated Unions (a.k.a. Tagged Unions)¶
When Union
is used with multiple submodels, you sometimes know exactly which submodel needs to
be checked and validated and want to enforce this.
To do that you can set the same field - let's call it my_discriminator
- in each of the submodels
with a discriminated value, which is one (or many) Literal
value(s).
For your Union
, you can set the discriminator in its value: Field(discriminator='my_discriminator')
.
Setting a discriminated union has many benefits:
- validation is faster since it is only attempted against one model
- only one explicit error is raised in case of failure
- the generated JSON schema implements the associated OpenAPI specification
from typing import Literal, Union
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: Union[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
try:
Model(pet={'pet_type': 'dog'}, n=1)
except ValidationError as e:
print(e)
"""
1 validation error for Model
pet.dog.barks
Field required [type=missing, input_value={'pet_type': 'dog'}, input_type=dict]
"""
Note
Using the typing.Annotated
fields syntax can be handy to regroup
the Union
and discriminator
information. See below for an example!
Warning
Discriminated unions cannot be used with only a single variant, such as Union[Cat]
.
Python changes Union[T]
into T
at interpretation time, so it is not possible for pydantic
to
distinguish fields of Union[T]
from T
.
Nested Discriminated Unions¶
Only one discriminator can be set for a field but sometimes you want to combine multiple discriminators.
You can do it by creating nested Annotated
types, e.g.:
from typing import Literal, Union
from typing_extensions import Annotated
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[Union[BlackCat, WhiteCat], Field(discriminator='color')]
class Dog(BaseModel):
pet_type: Literal['dog']
name: str
Pet = Annotated[Union[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
try:
Model(pet={'pet_type': 'cat', 'color': 'red'}, n='1')
except ValidationError as e:
print(e)
"""
1 validation error for Model
pet.cat
Input tag 'red' found using 'color' does not match any of the expected tags: 'black', 'white' [type=union_tag_invalid, input_value={'pet_type': 'cat', 'color': 'red'}, input_type=dict]
"""
try:
Model(pet={'pet_type': 'cat', 'color': 'black'}, n='1')
except ValidationError as e:
print(e)
"""
1 validation error for Model
pet.cat.black.black_name
Field required [type=missing, input_value={'pet_type': 'cat', 'color': 'black'}, input_type=dict]
"""