PydanticMiddleTechnical
Как валидировать generic-модели и сложные union-типы в Pydantic v2?
Generic-модели объявляются через Generic[T] + BaseModel, Union-типы обрабатываются с помощью Annotated и discriminator. Pydantic v2 поддерживает model_validate для всех этих конструкций.
Generic-модели в Pydantic v2
Для создания параметризованной модели нужно унаследоваться одновременно от BaseModel и Generic[T]. При инстанциировании Pydantic строит специализированный validator plan для конкретного типа параметра.
from pydantic import BaseModel
from typing import Generic, TypeVar, List, Optional
T = TypeVar("T")
class Page(BaseModel, Generic[T]):
items: List[T]
total: int
page: int
size: int
class UserDTO(BaseModel):
id: int
name: str
# Специализация: Page[UserDTO]
raw = {
"items": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}],
"total": 2,
"page": 1,
"size": 10,
}
page = Page[UserDTO].model_validate(raw)
print(page.items[0].name) # Alice
print(type(page.items[0])) # <class 'UserDTO'>
# Множественные TypeVar
K = TypeVar("K")
V = TypeVar("V")
class KVStore(BaseModel, Generic[K, V]):
key: K
value: V
metadata: Optional[dict] = None
store = KVStore[str, int].model_validate({"key": "count", "value": "42"})
print(store.value) # 42 (int)
Вложенные Generic
from pydantic import BaseModel
from typing import Generic, TypeVar, Optional
T = TypeVar("T")
class ApiResponse(BaseModel, Generic[T]):
data: Optional[T] = None
error: Optional[str] = None
status_code: int
class Product(BaseModel):
sku: str
price: float
response = ApiResponse[Product].model_validate({
"data": {"sku": "ABC-001", "price": "9.99"},
"status_code": 200,
})
print(response.data.price) # 9.99 (float)
Union-типы: базовый подход
Pydantic v2 поддерживает Union[A, B] и A | B (Python 3.10+). По умолчанию используется left-to-right matching: первый подходящий тип побеждает.
from pydantic import BaseModel
from typing import Union
class Cat(BaseModel):
type: str = "cat"
meow_volume: int
class Dog(BaseModel):
type: str = "dog"
bark_pitch: str
# Без дискриминатора — медленно и нестабильно на похожих схемах
class PetContainer(BaseModel):
pet: Union[Cat, Dog]
container = PetContainer.model_validate({"pet": {"type": "dog", "bark_pitch": "high"}})
# Может выбрать Cat, если поля совпадают — непредсказуемо!
Discriminated Union с Annotated
Для надёжной маршрутизации используйте Annotated + Field(discriminator=...). Pydantic смотрит на значение поля-дискриминатора и сразу выбирает нужный валидатор — без перебора.
from pydantic import BaseModel, Field
from typing import Union, Literal, Annotated
class Cat(BaseModel):
type: Literal["cat"]
meow_volume: int
class Dog(BaseModel):
type: Literal["dog"]
bark_pitch: str
class Parrot(BaseModel):
type: Literal["parrot"]
vocabulary_size: int
Animal = Annotated[
Union[Cat, Dog, Parrot],
Field(discriminator="type"),
]
class Zoo(BaseModel):
animals: list[Animal]
zoo = Zoo.model_validate({
"animals": [
{"type": "cat", "meow_volume": 8},
{"type": "dog", "bark_pitch": "low"},
{"type": "parrot", "vocabulary_size": 200},
]
})
print(type(zoo.animals[0])) # <class 'Cat'>
print(type(zoo.animals[1])) # <class 'Dog'>
TypeAdapter для валидации вне модели
from pydantic import TypeAdapter
from typing import Union, Literal, Annotated
from pydantic import Field
class CircleShape(BaseModel):
kind: Literal["circle"]
radius: float
class RectShape(BaseModel):
kind: Literal["rect"]
width: float
height: float
Shape = Annotated[Union[CircleShape, RectShape], Field(discriminator="kind")]
ta = TypeAdapter(Shape)
shape = ta.validate_python({"kind": "circle", "radius": "5.0"})
print(shape.radius) # 5.0
Подводные камни
- Generic без специализации —
Page.model_validate(raw)(безPage[UserDTO]) оставит полеitemsкакlist[Any]; специализируйте явно. - Порядок в Union без дискриминатора —
Union[int, str]всегда выберетintдля"42"через coercion; поменяйте порядок или используйте strict mode. - Literal с None —
Union[Cat, None]лучше записывать какOptional[Cat]; смешивание стилей путает IDE и генераторы схем. - Дискриминатор должен быть Literal — поле-дискриминатор обязано иметь тип
Literal["value"]в каждом варианте Union; если оно простоstr, Pydantic выброситPydanticUserError. - Рекурсивные Generic — модели, ссылающиеся на себя через Generic, требуют
model_rebuild()после определения класса. - JSON Schema для Generic —
Page[UserDTO].model_json_schema()корректно работает только на специализированном классе; неспециализированный вернётTкак{}.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.