Complete worked example
python
from typing import Annotated, Literal
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, ValidationError
class EventBase(BaseModel):
model_config = ConfigDict(extra="forbid")
class Email(EventBase):
kind: Literal["email"]
recipient: str = Field(min_length=1)
class Push(EventBase):
kind: Literal["push"]
device: str = Field(min_length=1)
Event = Annotated[Email | Push, Field(discriminator="kind")]
adapter = TypeAdapter(Event)
def inspect_example(payload):
try:
return {"value": adapter.validate_python(payload).model_dump()}
except ValidationError as error:
return {"error": sorted({e["type"] for e in error.errors()})}- Reasoning
- TypeAdapter validates the annotated union without adding a meaningless wrapper model. The kind value selects Email or Push, then the selected model checks its own fields. The shared base forbids extras so the accepted payload is not silently widened with fields belonging to a different command. Observe a valid email, an unsupported kind and a missing kind. The last two failures identify an invalid or absent union tag. The selected email variant can still fail a missing recipient check. Selecting a model and validating the contents are separate stages that should both appear in an API test suite.