A Pydantic model is a Python class where the type annotations ARE the schema. We write what we want, Pydantic validates and converts incoming data, and FastAPI uses the same model to generate OpenAPI docs.
In simple language — we describe our data once using normal Python type hints, and we get three things free: validation, JSON serialization, and auto-generated API docs.
Why we care
Without Pydantic, every endpoint becomes a wall of if not isinstance(...) checks. With it, we just declare the shape and FastAPI rejects bad requests before our handler runs.
The flow looks like this:
{"id": "42", ...}
parse + coerce
user.id == 42 (int)
The basics
We inherit from BaseModel and annotate fields. That’s it.
from pydantic import BaseModel
from datetime import datetime
class User(BaseModel):
id: int
email: str
is_active: bool = True
created_at: datetime | None = None
Pass it raw data and Pydantic coerces where it safely can (string "42" becomes int 42), or raises ValidationError.
user = User(id="42", email="manish@example.com")
print(user.id) # 42 (int, not str)
Use it in a FastAPI handler
Just declare the model as a parameter type. FastAPI does the rest — body parsing, validation, docs.
from fastapi import FastAPI
app = FastAPI()
@app.post("/users")
def create_user(user: User):
# user is already validated here
return {"created": user.email}
Dumping back to dict / JSON
We constantly need to serialize models — for DB inserts, logs, responses.
user.model_dump() # -> dict
user.model_dump_json() # -> JSON string
user.model_dump(exclude={"created_at"})
user.model_dump(exclude_none=True) # drop None fields
Pydantic v1 vs v2 — the diff that bites everyone
FastAPI now requires v2. If we copy old StackOverflow answers, things break. The big renames:
| v1 | v2 |
|---|---|
.dict() | .model_dump() |
.json() | .model_dump_json() |
.parse_obj(data) | .model_validate(data) |
@validator | @field_validator |
@root_validator | @model_validator |
class Config: | model_config = ConfigDict(...) |
orm_mode = True | from_attributes = True |
v2 is also ~17x faster — the core is written in Rust now. So the migration pain is worth it.
ConfigDict — model-level settings
When we need to tweak behavior (allow ORM objects, strip whitespace, forbid extra fields):
from pydantic import BaseModel, ConfigDict
class User(BaseModel):
model_config = ConfigDict(
from_attributes=True, # read from SQLAlchemy objects
str_strip_whitespace=True,
extra="forbid", # reject unknown fields
)
id: int
email: str
The takeaway — Pydantic models are the contract between the outside world and our handlers. We describe the shape once, get validation, docs, and serialization for free.