Validation Deep Dive — Field, field_validator & model_validator
IntermediateField() handles declarative constraints (bounds, lengths, patterns); @field_validator runs custom logic per field; @model_validator checks rules across fields — all producing structured 422s.
Overview
Pydantic v2 gives you three escalating validation tools. Field() covers the declarative 80%: numeric bounds (ge/gt/le/lt), string lengths and regex patterns, plus docs metadata. When a rule needs code — normalise a phone number, verify a pincode prefix, reject disposable email domains — @field_validator attaches a function to one field, and whatever it returns becomes the value (validators are also transformers). When a rule spans fields — end date after start date, at least one contact method present — @model_validator(mode="after") sees the whole model. Every failure joins the same 422 error list, so clients handle hand-written rules exactly like built-in ones. Keep validators pure: no DB calls, no I/O — that belongs in the service layer.
Field() Constraints + field_validator
Field() for declarative rules; @field_validator for logic. Raise ValueError with a human message — Pydantic wraps it into the standard error format. Returning a modified value normalises data at the boundary.
from pydantic import BaseModel, Field, field_validator
class StudentIn(BaseModel):
name: str = Field(min_length=2, max_length=60)
phone: str # custom rule below
cgpa: float = Field(ge=0, le=10)
pincode: str = Field(pattern=r"^[1-9][0-9]{5}$") # Indian pincode shape
@field_validator("phone")
@classmethod
def valid_indian_mobile(cls, v: str) -> str:
digits = "".join(ch for ch in v if ch.isdigit())
if digits.startswith("91") and len(digits) == 12:
digits = digits[2:] # strip +91 → normalise
if len(digits) != 10 or digits[0] not in "6789":
raise ValueError("must be a valid 10-digit Indian mobile")
return digits # ← transformed value is stored
@field_validator("name")
@classmethod
def tidy_name(cls, v: str) -> str:
return v.strip().title() # " asha rao " → "Asha Rao"
# StudentIn(name="asha rao", phone="+91 98765 43210", cgpa=8.7, pincode="413001")
# → phone stored as "9876543210", name as "Asha Rao"
# phone="12345" → 422: {"loc": ["body", "phone"],
# "msg": "Value error, must be a valid 10-digit Indian mobile"}Cross-Field Rules with model_validator
mode="after" runs once all fields are individually valid, receiving the constructed model — the place for relationships between fields. mode="before" sees the raw dict, useful for reshaping legacy payloads.
from datetime import date
from pydantic import BaseModel, model_validator
class DriveWindow(BaseModel):
company: str
starts_on: date
ends_on: date
online: bool = False
venue: str | None = None
@model_validator(mode="after")
def check_rules(self):
if self.ends_on < self.starts_on:
raise ValueError("ends_on must be on/after starts_on")
if not self.online and not self.venue:
raise ValueError("offline drives need a venue")
return self # always return the model
# {"company":"Wipro","starts_on":"2026-08-01","ends_on":"2026-07-20"}
# → 422 "ends_on must be on/after starts_on"
# {"company":"Wipro","starts_on":"2026-08-01","ends_on":"2026-08-02"}
# → 422 "offline drives need a venue" (online defaults to False)
# mode="before": reshape raw input before field validation —
class LegacyOrder(BaseModel):
order_id: int
amount: float
@model_validator(mode="before")
@classmethod
def unwrap_envelope(cls, data):
# old clients send {"data": {"order_id":..., "amount":...}}
return data.get("data", data)
# Rules of thumb:
# one field → field_validator · multiple fields → model_validator(after)
# reshaping raw payloads → model_validator(before)
# needs the database (uniqueness, existence) → NOT a validator; service layerKey Points to Remember
- 1Escalate: Field() constraints → field_validator → model_validator
- 2Validators return the stored value — normalise (strip, title, +91) at the boundary
- 3model_validator(mode="after") is the home for cross-field rules
- 4Raise ValueError with a clear message; keep validators free of I/O and DB calls
Interview Questions
Sign in to ask AriaValidate and normalise an Indian mobile number field — walk through your validator.
field_validator vs model_validator — when is each the right tool?
Why should "email must be unique" NOT be a Pydantic validator?
Ask Aria about Validation Deep Dive — Field, field_validator & model_validator
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.