Pydantic Models

intermediate fastapi pydantic

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:

Raw JSON
{"id": "42", ...}
Pydantic Model
parse + coerce
Typed object
user.id == 42 (int)
If anything fails → 422 response, handler never runs

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:

v1v2
.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 = Truefrom_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.