Discriminated Unions
Using Literal types, Union, and discriminator fields to handle multiple data shapes.
What if my data can have different shapes depending on its type? Like a notification that could be an email, SMS, or push notification — each with different fields?
This is one of the most powerful Pydantic patterns: discriminated unions. You create separate models for each shape, mark them with a Literal type field, and Pydantic picks the right model automatically:
from pydantic import BaseModel
from typing import Literal, Union
class EmailNotification(BaseModel):
type: Literal["email"]
to_address: str
subject: str
body: str
class SMSNotification(BaseModel):
type: Literal["sms"]
phone_number: str
message: str
class PushNotification(BaseModel):
type: Literal["push"]
device_id: str
title: str
The Literal["email"] field means that field must always be exactly the string "email". It's a constant that identifies which model to use.
How does Pydantic know which model to pick when I validate data?
Use a Union with a discriminator — a field that tells Pydantic which variant to try:
from pydantic import TypeAdapter
from typing import Annotated
from pydantic import Discriminator, Tag
Notification = Annotated[
Union[
Annotated[EmailNotification, Tag("email")],
Annotated[SMSNotification, Tag("sms")],
Annotated[PushNotification, Tag("push")]
],
Discriminator("type")
]
adapter = TypeAdapter(Notification)
result = adapter.validate_python({"type": "sms", "phone_number": "555-1234", "message": "Hello"})
print(type(result)) # <class 'SMSNotification'>
Pydantic looks at the type field, sees "sms", and knows to validate against SMSNotification. No trial-and-error, no ambiguity.
That's a lot of typing imports. Is there a simpler way?
For the common case where the discriminator is a field on the models, you can use a simpler pattern with a wrapper model:
class NotificationWrapper(BaseModel):
notification: Union[EmailNotification, SMSNotification, PushNotification] = Field(
discriminator="type"
)
data = {"notification": {"type": "email", "to_address": "a@b.com", "subject": "Hi", "body": "Hello"}}
wrapper = NotificationWrapper(**data)
print(type(wrapper.notification)) # EmailNotification
The Field(discriminator="type") tells Pydantic to use the type field to choose the right union member.
What's the advantage over just using a big model with lots of Optional fields?
Clarity and safety. With Optional fields, nothing stops you from creating an "email" notification without a to_address. With discriminated unions, each shape is a separate model with its own required fields. If type is "email", then to_address, subject, and body are all required — enforced by the type system:
# This would fail — EmailNotification requires subject and body
try:
adapter.validate_python({"type": "email", "to_address": "a@b.com"})
except Exception as e:
print("Missing fields caught!")
The discriminator pattern is how you model data that says "I'm one of these shapes" — API responses, event systems, message queues, configuration files.
Practice your skills
Sign up to write and run code in this lesson.
Discriminated Unions
Using Literal types, Union, and discriminator fields to handle multiple data shapes.
What if my data can have different shapes depending on its type? Like a notification that could be an email, SMS, or push notification — each with different fields?
This is one of the most powerful Pydantic patterns: discriminated unions. You create separate models for each shape, mark them with a Literal type field, and Pydantic picks the right model automatically:
from pydantic import BaseModel
from typing import Literal, Union
class EmailNotification(BaseModel):
type: Literal["email"]
to_address: str
subject: str
body: str
class SMSNotification(BaseModel):
type: Literal["sms"]
phone_number: str
message: str
class PushNotification(BaseModel):
type: Literal["push"]
device_id: str
title: str
The Literal["email"] field means that field must always be exactly the string "email". It's a constant that identifies which model to use.
How does Pydantic know which model to pick when I validate data?
Use a Union with a discriminator — a field that tells Pydantic which variant to try:
from pydantic import TypeAdapter
from typing import Annotated
from pydantic import Discriminator, Tag
Notification = Annotated[
Union[
Annotated[EmailNotification, Tag("email")],
Annotated[SMSNotification, Tag("sms")],
Annotated[PushNotification, Tag("push")]
],
Discriminator("type")
]
adapter = TypeAdapter(Notification)
result = adapter.validate_python({"type": "sms", "phone_number": "555-1234", "message": "Hello"})
print(type(result)) # <class 'SMSNotification'>
Pydantic looks at the type field, sees "sms", and knows to validate against SMSNotification. No trial-and-error, no ambiguity.
That's a lot of typing imports. Is there a simpler way?
For the common case where the discriminator is a field on the models, you can use a simpler pattern with a wrapper model:
class NotificationWrapper(BaseModel):
notification: Union[EmailNotification, SMSNotification, PushNotification] = Field(
discriminator="type"
)
data = {"notification": {"type": "email", "to_address": "a@b.com", "subject": "Hi", "body": "Hello"}}
wrapper = NotificationWrapper(**data)
print(type(wrapper.notification)) # EmailNotification
The Field(discriminator="type") tells Pydantic to use the type field to choose the right union member.
What's the advantage over just using a big model with lots of Optional fields?
Clarity and safety. With Optional fields, nothing stops you from creating an "email" notification without a to_address. With discriminated unions, each shape is a separate model with its own required fields. If type is "email", then to_address, subject, and body are all required — enforced by the type system:
# This would fail — EmailNotification requires subject and body
try:
adapter.validate_python({"type": "email", "to_address": "a@b.com"})
except Exception as e:
print("Missing fields caught!")
The discriminator pattern is how you model data that says "I'm one of these shapes" — API responses, event systems, message queues, configuration files.
Practice your skills
Sign up to write and run code in this lesson.