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_jsonvalidate_python принимает Python-объекты (может выполнять coercion), validate_json принимает JSON-строку или байты и работает быстрее за счёт Rust-парсера.
  • TypeAdapter(Optional[X]) и NoneTypeAdapter(Optional[int]).validate_python(None) корректно возвращает None, но validate_json("null") тоже нужно проверять отдельно.

Common mistakes

  • Описывать typeadapter только как термин и не показывать механизм на минимальном примере.
  • Игнорировать ошибки, пустые данные, конкурентный доступ или границы транзакции.
  • Не связывать поведение с официальным контрактом Pydantic и реальной эксплуатацией.

What the interviewer is testing

  • Объясняет typeadapter через последовательность действий, а не через набор ключевых слов.
  • Приводит короткий кодовый пример или production-сценарий с ожидаемым поведением.
  • Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.

Sources

Related topics