Difficulty: Intermediate
While Pydantic's automatic type validation handles many cases, real applications often need custom validation rules. Pydantic's Field function and validator decorators let you add constraints like minimum values, string patterns, and custom logic that runs during model creation.
The Field function is Pydantic's equivalent of FastAPI's Query and Path. It adds metadata and validation constraints to model fields. You can set default values, minimum/maximum values for numbers, min/max length for strings, regex patterns, descriptions, and examples. Field is used inside model definitions, while Query/Path are used in function signatures.
For custom validation logic that goes beyond simple constraints, Pydantic provides the @field_validator decorator. A field validator is a class method that receives the field value, validates or transforms it, and returns the (possibly modified) value. If validation fails, it raises a ValueError with a descriptive message.
The @model_validator decorator validates the entire model after all individual field validations pass. This is useful for cross-field validation - for example, ensuring that end_date is after start_date, or that password and confirm_password match. Model validators receive the entire model instance and can check relationships between fields.
Pydantic v2 introduced a cleaner validator syntax using mode='before' and mode='after'. 'before' validators run before Pydantic's type validation and can transform raw input. 'after' validators run after type validation and receive properly typed values. By default, field validators run in 'after' mode.
Field constraints can also be applied using Annotated types with Pydantic's functional validators. This approach is more composable and reusable than class-based validators. You can define validation functions once and apply them to fields across multiple models.
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(
min_length=1,
max_length=100,
description="Product name",
examples=["Widget Pro"],
)
price: float = Field(
gt=0,
description="Price in USD, must be positive",
examples=[29.99],
)
quantity: int = Field(
default=0,
ge=0,
le=10000,
description="Stock quantity",
)
sku: str = Field(
pattern=r"^[A-Z]{2}-\d{4}quot;,
description="SKU format: two uppercase letters, dash, four digits",
examples=["AB-1234"],
)
# Valid
Product(name="Widget", price=9.99, sku="AB-1234")
# Invalid: price must be > 0
# Product(name="Widget", price=0, sku="AB-1234") -> ValidationError
# Invalid: SKU format wrong
# Product(name="Widget", price=9.99, sku="abc") -> ValidationError
Field() adds constraints and metadata to model fields. gt/ge/lt/le work for numbers. min_length/max_length/pattern work for strings. description and examples appear in the generated JSON Schema and Swagger UI.
from pydantic import BaseModel, field_validator
class UserRegister(BaseModel):
username: str
email: str
password: str
age: int
@field_validator("username")
@classmethod
def username_must_be_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError("Username must be alphanumeric")
return v.lower() # Normalize to lowercase
@field_validator("email")
@classmethod
def email_must_contain_at(cls, v: str) -> str:
if "@" not in v:
raise ValueError("Invalid email address")
return v.lower()
@field_validator("password")
@classmethod
def password_must_be_strong(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("Password must be at least 8 characters")
if not any(c.isupper() for c in v):
raise ValueError("Password must contain an uppercase letter")
if not any(c.isdigit() for c in v):
raise ValueError("Password must contain a digit")
return v
@field_validator("age")
@classmethod
def age_must_be_valid(cls, v: int) -> int:
if v < 13 or v > 120:
raise ValueError("Age must be between 13 and 120")
return v
Field validators are class methods decorated with @field_validator. They receive the field value, perform validation, and return the value (possibly transformed). Raising ValueError triggers a validation error. The username validator also normalizes the value to lowercase.
from pydantic import BaseModel, model_validator
from datetime import date
class DateRange(BaseModel):
start_date: date
end_date: date
@model_validator(mode="after")
def end_after_start(self):
if self.end_date <= self.start_date:
raise ValueError("end_date must be after start_date")
return self
class PasswordChange(BaseModel):
current_password: str
new_password: str
confirm_password: str
@model_validator(mode="after")
def passwords_match(self):
if self.new_password != self.confirm_password:
raise ValueError("new_password and confirm_password must match")
if self.new_password == self.current_password:
raise ValueError("New password must differ from current")
return self
# Valid
DateRange(start_date="2026-01-01", end_date="2026-12-31")
# Invalid: end before start
# DateRange(start_date="2026-12-31", end_date="2026-01-01")
# -> ValueError: end_date must be after start_date
Model validators with mode='after' run after all field validations succeed. They receive the fully constructed model instance (self) and can validate relationships between fields. This is the standard pattern for cross-field validation.
Field, Validators, field_validator, model_validator, Constraints