TypeAdapter: validating any type

Pydantic · Advanced · 10 min

Not everything deserves a model class, but lists, dicts and TypedDicts at boundaries still need validation.

BaseModel gives validation methods to classes. TypeAdapter gives the same powers to any type:

ta = TypeAdapter(list[int])
ta.validate_python(["1", 2])      # [1, 2]
ta.validate_json(b"[1, 2]")
ta.dump_json([1, 2])
ta.json_schema()

Typical uses:

  • top-level collections: a JSON array of objects, TypeAdapter(list[User])
  • TypedDicts, when you want plain dicts (no attribute access, no model overhead) but validated keys and types
  • standard library dataclasses, NamedTuples and Annotated constraint types
  • unions that are not wrapped in a model

Creating a TypeAdapter builds a validator, which costs time (it compiles a core schema). Create adapters once, at module level, and reuse them; building one inside a hot loop or per request is a common performance bug.

Pydantic also has its own @pydantic.dataclasses.dataclass, a drop-in for the standard decorator that validates on __init__. It suits code that wants dataclass semantics; BaseModel has more features (config inheritance, model_* methods), so prefer models for API schemas.

validate_call is the function equivalent: decorate a function and its arguments are validated against their annotations on every call, with an optional validate_return=True.

Example

from typing import TypedDict
from pydantic import TypeAdapter, validate_call

class Row(TypedDict):
    id: int
    score: float

ROWS = TypeAdapter(list[Row])          # build once, reuse

rows = ROWS.validate_json(b'[{"id": "1", "score": 9}, {"id": 2, "score": "7.5"}]')
print(rows)              # [{'id': 1, 'score': 9.0}, {'id': 2, 'score': 7.5}]
print(type(rows[0]))     # <class 'dict'>
print(ROWS.json_schema()["$defs"]["Row"]["required"])   # ['id', 'score']

@validate_call
def repeat(text: str, times: int) -> str:
    return text * times

print(repeat("ab", "3"))   # ababab

Remember

  • TypeAdapter(T) gives validate_python/validate_json/dump/json_schema for any type
  • Good for top-level lists, TypedDicts and unions
  • Build adapters once; construction compiles a validator
  • @validate_call validates function arguments

Watch out for

  • Constructing a TypeAdapter inside a request handler or loop.

Check yourself

  1. Why define ROWS = TypeAdapter(list[Row]) at module level instead of inside the function that uses it?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.pydantic.dev/latest/concepts/type_adapter/