PydanticMiddleCoding
Что такое TypeAdapter в Pydantic v2 и как он валидирует данные вне модели?
TypeAdapter позволяет использовать всю мощь валидации Pydantic для произвольных Python-типов (list, dict, Union, примитивы) без создания класса BaseModel.
Зачем нужен TypeAdapter
Методы model_validate() и model_validate_json() доступны только для подклассов BaseModel. Но иногда нужно провалидировать встроенный тип, List[SomeModel], Union, или аннотированный тип без создания обёртки. TypeAdapter решает эту задачу.
Базовый синтаксис
from pydantic import TypeAdapter, ValidationError
from typing import List, Dict, Optional
from datetime import datetime
# TypeAdapter для списка целых чисел
ta_int_list = TypeAdapter(List[int])
result = ta_int_list.validate_python(["1", "2", "3"]) # coercion
print(result) # [1, 2, 3]
# TypeAdapter для datetime
ta_dt = TypeAdapter(datetime)
dt = ta_dt.validate_python("2024-01-15T12:00:00")
print(dt) # 2024-01-15 12:00:00
print(type(dt)) # <class 'datetime.datetime'>
# Валидация из JSON
ta_dict = TypeAdapter(Dict[str, int])
d = ta_dict.validate_json('{"a": 1, "b": 2}')
print(d) # {'a': 1, 'b': 2}
# Ошибка валидации
try:
ta_int_list.validate_python(["not", "numbers"])
except ValidationError as e:
print(e)
Валидация списков Pydantic-моделей
from pydantic import BaseModel, TypeAdapter
from typing import List
import json
class UserDTO(BaseModel):
id: int
name: str
active: bool = True
# Один раз создаём адаптер — он кеширует validator plan
UserListAdapter = TypeAdapter(List[UserDTO])
raw_json = b'[{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob", "active": false}]'
users = UserListAdapter.validate_json(raw_json)
print(users[0].name) # Alice
print(users[1].active) # False
# Сериализация списка обратно в JSON
json_out = UserListAdapter.dump_json(users)
print(json_out) # b'[{"id":1,"name":"Alice","active":true},...]'
# Генерация JSON Schema для списка
schema = UserListAdapter.json_schema()
print(schema["type"]) # array
TypeAdapter с Annotated-типами
from pydantic import TypeAdapter
from typing import Annotated
from pydantic import Field
# Валидация примитива с ограничениями без создания модели
PositiveInt = Annotated[int, Field(gt=0, le=1_000_000)]
ta = TypeAdapter(PositiveInt)
print(ta.validate_python(42)) # 42
print(ta.validate_python("100")) # 100 (coercion)
try:
ta.validate_python(-1)
except Exception as e:
print(e) # Input should be greater than 0
# Переиспользуемый тип для email
from pydantic import EmailStr
EmailAdapter = TypeAdapter(EmailStr)
email = EmailAdapter.validate_python("user@example.com")
Сравнение с RootModel и BaseModel
from pydantic import BaseModel, RootModel, TypeAdapter
from typing import List
# Вариант 1: BaseModel (нужна обёртка с полем)
class UserListModel(BaseModel):
items: List[dict]
# Вариант 2: RootModel (корневое значение, переиспользуемый класс)
class UserList(RootModel[List[dict]]):
pass
# Вариант 3: TypeAdapter (без класса, одноразово или через переменную)
ta = TypeAdapter(List[dict])
result = ta.validate_python([{"id": 1}])
# TypeAdapter предпочтителен когда:
# - тип встроенный (list, dict, int)
# - нужна одноразовая валидация в функции
# - не нужен именованный класс с методами
TypeAdapter в FastAPI для нестандартных ответов
from fastapi import FastAPI
from pydantic import TypeAdapter, BaseModel
from typing import List
app = FastAPI()
class Item(BaseModel):
id: int
name: str
ItemListAdapter = TypeAdapter(List[Item])
@app.get("/items")
def get_items() -> List[Item]:
raw = [{"id": "1", "name": "Laptop"}, {"id": "2", "name": "Phone"}]
return ItemListAdapter.validate_python(raw)
Подводные камни
- Создавайте TypeAdapter один раз — инициализация компилирует validator plan в Rust; создание
TypeAdapter(List[UserDTO])внутри цикла или функции на каждый вызов — дорогостоящая операция. Выносите в модульный уровень или атрибут класса. - TypeAdapter не поддерживает model_config — настройки типа
populate_by_nameилиstrictнельзя передать черезTypeAdapter; для тонкой настройки создавайтеBaseModel. - dump_json() возвращает bytes — в отличие от
BaseModel.model_dump_json(), который возвращаетstr, методTypeAdapter.dump_json()возвращаетbytes. - validate_python vs validate_json —
validate_pythonпринимает Python-объекты (может выполнять coercion),validate_jsonпринимает JSON-строку или байты и работает быстрее за счёт Rust-парсера. - TypeAdapter(Optional[X]) и None —
TypeAdapter(Optional[int]).validate_python(None)корректно возвращаетNone, ноvalidate_json("null")тоже нужно проверять отдельно.
Common mistakes
- Описывать typeadapter только как термин и не показывать механизм на минимальном примере.
- Игнорировать ошибки, пустые данные, конкурентный доступ или границы транзакции.
- Не связывать поведение с официальным контрактом Pydantic и реальной эксплуатацией.
What the interviewer is testing
- Объясняет typeadapter через последовательность действий, а не через набор ключевых слов.
- Приводит короткий кодовый пример или production-сценарий с ожидаемым поведением.
- Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.