API envelopes and paginated responses repeat the same wrapper around different payloads.
A generic model has fields typed with a TypeVar. In Python 3.12+ syntax:
class Page[T](BaseModel):
items: list[T]
total: int(On older versions, inherit from BaseModel, Generic[T].)
Parametrizing it, Page[User], creates a real subclass with T replaced by User, so validation, serialization and JSON Schema are fully typed for that payload. Pydantic caches parametrized classes, so Page[User] is Page[User].
Rules worth knowing:
- An unparametrized
PagevalidatesTasAny, so items are passed through without validation. Always use the parametrized class at boundaries. TypeVarbounds and defaults are respected: withT: Number(orbound=), unparametrized use validates against the bound.- Partial parametrization works with several TypeVars:
Pair[int, T]. - A generic model can inherit from another; parametrize the subclass with new or same TypeVars.
Each parametrization builds a new class and validator, so create them at import time (UserPage = Page[User]) rather than dynamically per request.
Generic models combine with everything else: a Page[Event] where Event is a discriminated union, or a Response[T] envelope that FastAPI uses as response_model=Response[User] to document each endpoint precisely.
Example
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
class Page[T](BaseModel):
items: list[T]
total: int
next_cursor: str | None = None
UserPage = Page[User]
p = UserPage.model_validate({"items": [{"id": "1", "name": "Ann"}], "total": 1})
print(type(p.items[0]).__name__, p.items[0].id) # User 1
print(UserPage.__name__) # Page[User]
print(Page[User] is UserPage) # True
raw = Page.model_validate({"items": [{"id": "1"}], "total": 1})
print(raw.items) # [{'id': '1'}] (T unparametrized -> Any, no validation)Remember
- class Page[T](BaseModel) + Page[User] gives a fully typed model
- Parametrized classes are cached and should be created at import time
- An unparametrized generic validates its TypeVar fields as Any (or the bound)
Watch out for
- Validating with the bare generic class and assuming the items were checked.