60.3 Pydantic Models: Request and Response Validation
Right, let’s talk about the unsung hero of your FastAPI application: Pydantic models. This is where the magic happens, and I don’t use that term lightly. Most frameworks make you write a ton of boilerplate code to validate incoming data and outgoing responses. You end up with a rats’ nest of if-else statements checking if email is actually an email, or if age is a positive integer. It’s tedious, error-prone, and soul-crushingly boring.
FastAPI, in its infinite wisdom, says, “Nah, we’re not doing that.” Instead, it leans hard on Pydantic. Think of a Pydantic model as your single source of truth. It’s a class that defines the exact shape, type, and validation rules for your data. You define it once, and FastAPI uses it to automatically:
- Validate incoming request bodies (so you don’t get garbage data).
- Serialize and validate outgoing response bodies (so you don’t accidentally leak your users’ password hashes).
- Generate OpenAPI documentation (so your API docs are always, always correct).
It’s like having a brilliant, pedantic bouncer for your data club. Let’s build one.
from pydantic import BaseModel, EmailStr, conint
from typing import Optional
class UserCreate(BaseModel):
email: EmailStr # Not just a string, an *email* string
password: str
full_name: Optional[str] = None
class UserOut(BaseModel):
id: int
email: EmailStr
full_name: Optional[str] = None
class Config:
orm_mode = True
See that? We just defined the rules. To create a user, you must provide an email (which Pydantic will validate) and a password. The full_name is optional. Notice we have two different models: one for input (UserCreate) and one for output (UserOut). This is a critical best practice. Your input might require a password, but you should never, ever send that password back in a response. Separate models give you absolute control.
The Validation Vanguard
Pydantic’s type system is what makes it so powerful. We used EmailStr above, which is a far cry from a simple str. It’s got built-in validation. Let’s get more specific.
from pydantic import BaseModel, Field, HttpUrl, validator
class PremiumArticle(BaseModel):
title: str = Field(..., min_length=5, max_length=100) # Ellipsis means required
content: str
premium_thumbnail: HttpUrl # Validates that it's a real URL
rating: int = Field(ge=1, le=5) # Greater-than-or-equal-to 1, less-than-or-equal-to 5
@validator('title')
def title_must_not_be_clickbait(cls, v):
if v.startswith("You Won't Believe This"):
raise ValueError('Seriously? Try harder.')
return v
The Field function lets you add extra constraints right in the model definition. But for truly custom nonsense, you drop a @validator decorator. This is your escape hatch for any business logic that’s too weird for standard types. The validator gets the value, and you can transform it or raise a ValueError if it’s unacceptable. FastAPI will catch that error and automatically return a 422 Unprocessable Entity response with a beautifully formatted error message detailing what went wrong. No manual error handling required.
ORM Mode: The Bridge to Your Database
Here’s a classic “gotcha.” Let’s say you use SQLAlchemy and you fetch a user from the database. You can’t just shove that SQLAlchemy model instance into a UserOut model. They’re different types of objects. This is where orm_mode comes in.
Remember the Config class inside UserOut? That orm_mode = True tells Pydantic: “Hey, when you see an object that isn’t a dict, try to read its data like you would from an ORM (i.e., using object attributes like user.id, not dictionary keys like user['id']).”
This is a lifesaver.
# Inside your path operation function
db_user = db.query(User).filter(User.id == user_id).first()
# This would fail without orm_mode. With it, it works perfectly.
return UserOut.from_orm(db_user)
Without this, you’d be stuck manually building a dict like {"id": db_user.id, "email": db_user.email...}. Yuck. orm_mode automates that drudgery. It’s not magic; it’s just well-designed pragmatism.
Common Pitfalls and the Wisdom of Experience
The Optional Trap: Remember, in Pydantic,
optional_field: Optional[str]is different fromoptional_field: Optional[str] = None. The first is a required field that can be None. The second is an optional field that defaults to None. FastAPI treats the first as a required field in requests, which will cause a validation error if missing. Use the second version for truly optional fields.Nested Models are Your Friend: Don’t be afraid to go deep. Pydantic handles nested models beautifully.
class Item(BaseModel): name: str class UserWithItems(UserOut): items: List[Item] = []This will validate and document the entire nested structure automatically.
Performance? It’s Blazingly Fast: Pydantic is written in Rust (the core, anyway) and is ridiculously fast. Don’t worry about the validation overhead. The time you save not debugging invalid data or incorrect API docs is worth infinitely more than the microseconds it takes to validate a payload.
The bottom line is this: Pydantic models are your application’s contract. They are the law. By defining them rigorously, you force your API to be consistent, secure, and self-documenting. It’s one of the few things in web development that feels like you’re actually building with precision tools instead of duct tape and hope. Use them relentlessly.