Cheatsheet for Pydantic v2 validators.
field_validator
from pydantic import BaseModel, field_validator
class M(BaseModel):
email: str
age: int
@field_validator("email", mode="before")
@classmethod
def lower_email(cls, v):
return v.lower().strip() if isinstance(v, str) else v
@field_validator("age") # default mode="after"
@classmethod
def positive_age(cls, v: int) -> int:
if v < 0:
raise ValueError("age must be non-negative")
return v
mode="before": raw input. mode="after": typed value.
Multi-field validator
@field_validator("email", "alt_email")
@classmethod
def lower(cls, v: str) -> str:
return v.lower()
Apply same validator to several fields.
ValidationInfo (cross-field)
from pydantic import ValidationInfo
@field_validator("password_confirm")
@classmethod
def passwords_match(cls, v, info: ValidationInfo):
if v != info.data.get("password"):
raise ValueError("passwords don't match")
return v
info.data = already-validated fields (declaration order matters).
model_validator
from pydantic import model_validator
class Order(BaseModel):
quantity: int
price: float
@model_validator(mode="after")
def total_under_limit(self):
if self.quantity * self.price > 1_000_000:
raise ValueError("order too large")
return self
mode="after": typed model; access typed attrs.
mode="before": raw dict; transform input.
model_validator mode=‘before’
@model_validator(mode="before")
@classmethod
def normalize(cls, data):
if isinstance(data, dict):
if "name" in data and isinstance(data["name"], str):
data["name"] = data["name"].strip()
return data
Annotated validators (reusable)
from typing import Annotated
from pydantic import BeforeValidator, AfterValidator
def lowercase(v: str) -> str:
return v.lower()
def positive(v: int) -> int:
if v <= 0: raise ValueError("must be positive")
return v
LowercaseEmail = Annotated[str, AfterValidator(lowercase)]
PositiveInt = Annotated[int, AfterValidator(positive)]
class User(BaseModel):
email: LowercaseEmail
age: PositiveInt
Composable; reusable.
WrapValidator (wrap default)
from pydantic import WrapValidator
from pydantic import ValidatorFunctionWrapHandler
from typing import Any
def trim_then_validate(v: Any, handler: ValidatorFunctionWrapHandler) -> str:
if isinstance(v, str): v = v.strip()
return handler(v) # delegate to original
TrimmedStr = Annotated[str, WrapValidator(trim_then_validate)]
PlainValidator (replace default)
from pydantic import PlainValidator
def my_validate(v):
return str(v).strip()
class M(BaseModel):
name: Annotated[str, PlainValidator(my_validate)]
No default validation; only your function.
Context-aware validation
@field_validator("email")
@classmethod
def check(cls, v, info: ValidationInfo):
if info.context and info.context.get("check_db"):
if email_exists(v):
raise ValueError("email taken")
return v
User.model_validate({"email": "..."}, context={"check_db": True})
Validator order
mode="before"validators (Annotated + field_validator).- Type coercion / parsing.
mode="after"validators.
For the whole model:
model_validator(mode="before").- Field validation (per above).
model_validator(mode="after").
Raise ValueError, not ValidationError
@field_validator("age")
@classmethod
def positive(cls, v):
if v < 0:
raise ValueError("age must be non-negative") # converted to ValidationError with loc
return v
Don’t raise ValidationError directly from validators.
Use cases
Normalize
@field_validator("phone", mode="before")
@classmethod
def normalize(cls, v):
if isinstance(v, str):
return re.sub(r"\D", "", v)
return v
Cross-field
@model_validator(mode="after")
def end_after_start(self):
if self.end_date <= self.start_date:
raise ValueError("end must be after start")
return self
Conditional required
@model_validator(mode="after")
def kind_specific(self):
if self.type == "alpha" and not self.alpha_field:
raise ValueError("alpha_field required when type=alpha")
return self
Common mistakes
- Forgetting to
return v— silently sets None. - Cross-field check in
field_validator— usemodel_validatorinstead. - Heavy work / DB lookup in validators — slow + impure.
- Mutating input dict in
model_validator(mode="before")— sometimes works, sometimes not.
Read this next
If you want my reusable validator library, 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 .