Skip to content

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]
    """