Cheatsheet for extending Pydantic beyond BaseModel.
Custom type via Annotated (easiest)
from typing import Annotated
from pydantic import BeforeValidator, AfterValidator
def coerce(v):
if isinstance(v, str): return int(v.strip())
return v
def positive(v: int):
if v <= 0: raise ValueError("must be positive")
return v
PositiveInt = Annotated[int, BeforeValidator(coerce), AfterValidator(positive)]
class M(BaseModel):
count: PositiveInt
Composable.
get_pydantic_core_schema
For deep integration:
from pydantic import GetCoreSchemaHandler
from pydantic_core import CoreSchema, core_schema
class MyType:
def __init__(self, value: str):
self.value = value
@classmethod
def __get_pydantic_core_schema__(cls, source_type, handler: GetCoreSchemaHandler) -> CoreSchema:
return core_schema.no_info_after_validator_function(
cls,
core_schema.str_schema(),
)
Total control over validation + serialization. Most apps don’t need this.
TypeAdapter (non-BaseModel)
from pydantic import TypeAdapter
UserListAdapter = TypeAdapter(list[User])
DictAdapter = TypeAdapter(dict[str, int])
users = UserListAdapter.validate_python(raw_list)
d = DictAdapter.validate_json('{"a": 1}')
DictAdapter.dump_json(d)
Use module-scope (don’t recreate per call).
RootModel
from pydantic import RootModel
class Names(RootModel[list[str]]):
pass
n = Names(["Alice", "Bob"])
n.root # ["Alice", "Bob"]
For JSON that’s a list/primitive at the top level.
pydantic.dataclasses
from pydantic.dataclasses import dataclass
@dataclass
class User:
id: int
name: str
User(id=1, name="Alice")
Stdlib-dataclass syntax + Pydantic validation.
Compose Annotated metadata
from typing import Annotated
from pydantic import Field, BeforeValidator, AfterValidator, StringConstraints
Username = Annotated[
str,
StringConstraints(min_length=3, max_length=32, pattern=r"^[a-z0-9_]+$"),
BeforeValidator(lambda v: v.lower().strip() if isinstance(v, str) else v),
AfterValidator(lambda v: v),
Field(description="Username"),
]
Multiple metadata items combined.
get_pydantic_json_schema
For custom JSON schema:
from pydantic import GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
class MyType:
@classmethod
def __get_pydantic_json_schema__(cls, schema, handler):
result = handler(schema)
result["title"] = "MyType"
result["description"] = "Custom"
return result
Conditional fields via model_validator
from pydantic import model_validator
class M(BaseModel):
type: str
a: str | None = None
b: int | None = None
@model_validator(mode="after")
def required_by_type(self):
if self.type == "alpha" and self.a is None:
raise ValueError("a required when type=alpha")
return self
Or use a discriminated union (cleaner).
Validate per-call context
from pydantic import ValidationInfo
class User(BaseModel):
email: str
@field_validator("email")
@classmethod
def check(cls, v, info: ValidationInfo):
ctx = info.context or {}
if ctx.get("strict") and "@example.com" in v:
raise ValueError("disallowed domain")
return v
User.model_validate({"email": "..."}, context={"strict": True})
from_attributes for ORM
class UserOut(BaseModel):
id: int
email: str
model_config = {"from_attributes": True}
UserOut.model_validate(sa_user) # SQLAlchemy → Pydantic
Pickle support
import pickle
data = pickle.dumps(user)
restored = pickle.loads(data)
BaseModels are picklable.
Common mistakes
- Subclassing
int/strfor custom types — loses Pydantic features. - TypeAdapter created per call — schema compilation per call.
- RootModel for multi-field — overkill; use BaseModel.
- Mixing pydantic.dataclass and BaseModel inheritance — quirky.
Read this next
If you want my custom-type library + TypeAdapter helpers, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .