
Input Validation: Custom Logic and Validators
Master complex validation. Learn how to write custom validator functions for cross-field checks and business logic in Pydantic.
Input Validation: Custom Logic and Validators
Basic types (int, str) and field constraints (gt, min_length) cover 80% of use cases. But what about the other 20%? What if you need to ensure a password contains a special character, or that an end_date is after a start_date?
In this lesson, we learn to write Custom Validators in Pydantic.
1. The @field_validator Decorator (Pydantic v2)
If you want to validate a specific field with custom logic, use the @field_validator decorator.
from pydantic import BaseModel, field_validator
class UserCreate(BaseModel):
username: str
password: str
@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 long')
if not any(char.isdigit() for char in v):
raise ValueError('Password must contain at least one digit')
return v
2. Multi-Field Validation
Sometimes a field's validity depends on another field. In Pydantic v2, we use @model_validator.
from pydantic import BaseModel, model_validator
class Event(BaseModel):
start_time: int
end_time: int
@model_validator(mode='after')
def check_time_flow(self) -> 'Event':
if self.end_time <= self.start_time:
raise ValueError('End time must be after start time')
return self
Note: mode='after' ensures the basic type validation has already happened before your custom logic runs.
3. The Power of Annotated
Modern FastAPI code uses the Annotated type to keep models clean and reusable.
from typing import Annotated
from pydantic import Field
# Define a reusable "Username" type
Username = Annotated[str, Field(min_length=3, max_length=20, pattern="^[a-z0-9]+$")]
class User(BaseModel):
name: Username
alias: Username
4. Handling Error Messages
When a custom validator raises a ValueError, FastAPI automatically catches it and returns it to the user in a standardized JSON format.
Example Response for a weak password:
{
"detail": [
{
"type": "value_error",
"loc": ["body", "password"],
"msg": "Value error, Password must contain at least one digit",
"input": "secret"
}
]
}
Summary
@field_validator: Best for single-field logic (Regex, string checks).@model_validator: Critical for logic that compares multiple fields.Annotated: Helps you stay DRIVE (Don't Repeat Yourself) by creating reusable types.- Automatic Mapping: Custom errors are automatically delivered via FastAPI's 422 responses.
In the next lesson, we'll focus on the "Other Half": Output Models and Response Shaping.
Exercise: The Date Checker
Design a model for a Flight. It has a departure_city and an arrival_city.
Add a model validator to ensure that departure_city and arrival_city are not the same. (You can't fly from New York to New York!)