Rules that span fields or need code, like normalizing input or checking that end > start, live in validators.
When constraints are not enough, write a validator.
@field_validator("name") on a classmethod validates one or more fields. The mode decides when it runs:
mode="after"(the default) runs after Pydantic's own validation, so you receive a value of the right type. Most validators should be after validators.mode="before"runs on the raw input, before type conversion. Use it to reshape input, for example splitting"a,b"into a list.mode="wrap"receives ahandleryou call to run the inner validation, so you can act before and after or catch its errors.mode="plain"replaces Pydantic's validation for that field entirely.
The validator must return the value to keep; forgetting the return sets the field to None. Raise ValueError to reject; the message lands in the error list.
@model_validator(mode="after") is an instance method that sees the fully built model, the right place for cross-field rules such as end >= start; it must return self. A mode="before" model validator is a classmethod receiving the raw data, often a dict but possibly anything.
info: ValidationInfo as a second parameter gives access to info.data, the fields validated so far (in definition order), and the validation context passed via model_validate(data, context=...).
Validators do not run on defaults unless you set validate_default=True on the field, and they do not run on attribute assignment unless validate_assignment=True is configured.
Example
from datetime import date
from pydantic import BaseModel, ValidationError, field_validator, model_validator
class Booking(BaseModel):
tags: list[str] = []
start: date
end: date
@field_validator("tags", mode="before")
@classmethod
def split_csv(cls, v):
return v.split(",") if isinstance(v, str) else v
@model_validator(mode="after")
def check_dates(self):
if self.end < self.start:
raise ValueError("end must not be before start")
return self
b = Booking(tags="vip,late", start="2025-01-01", end="2025-01-03")
print(b.tags) # ['vip', 'late']
try:
Booking(start="2025-01-05", end="2025-01-01")
except ValidationError as e:
print(e.errors()[0]["msg"]) # Value error, end must not be before startRemember
- after validators get typed values; before validators get raw input
- Always return the value (field) or self (model after-validator)
- Use a model validator for rules that involve several fields
Watch out for
- Forgetting the return statement, which silently sets the field to None.
- Expecting validators to run on defaults or on assignment without enabling it.