FASTAPI / 2. PYDANTIC & REQUEST BODIES

Pydantic Models & Request Bodies

Type-safe data validation for everything that comes in and goes out


EXPLANATION

Pydantic is the heart of FastAPI. Every piece of data that enters or leaves your API is validated, coerced, and documented through Pydantic models.

A Pydantic model is a Python class that inherits from BaseModel. Every field with a type annotation is automatically validated. Pydantic v2 (the current version) is compiled in Rust — validation is extremely fast.

When you type-hint a function parameter as a Pydantic model, FastAPI knows it must come from the request body (JSON). It parses the JSON → validates every field → calls your function with a fully valid Python object. Invalid data returns 422 with detailed error messages automatically.

Field() adds constraints and metadata:
• gt, ge, lt, le → numeric bounds
• min_length, max_length → string bounds
• pattern → regex validation
• description, example → shows in /docs

Nested models work naturally — a model can have fields that are themselves Pydantic models, or lists of models. FastAPI handles deep serialization automatically.

model_config with json_schema_extra adds example payloads to /docs so frontend developers know exactly what to send.

Optional fields use type | None = None (Python 3.10+) or Optional[type] = None. Required fields have no default.

Validators using @field_validator let you write custom validation logic — password confirmation, cross-field checks, business rules.

ARCHITECTURE

REQUEST JSON:
  {
    "name": "Laptop",
    "price": 999.99,
    "in_stock": true,
    "tags": ["electronics", "computers"]
  }
         ↓ FastAPI parses body
  ┌──────────────────────────────────────────┐
  │  Pydantic validates field by field:      │
  │  name: str         → "Laptop"     ✓     │
  │  price: float      → 999.99       ✓     │
  │  in_stock: bool    → True         ✓     │
  │  tags: list[str]   → [...]        ✓     │
  │  description: None → None (optional) ✓  │
  └──────────────────────────────────────────┘
         ↓ Your function receives
  item: Item  ← fully validated Python object
  item.name   → "Laptop"
  item.price  → 999.99  (float, not string)

CODE

PYTHON
1from fastapi import FastAPI
2from pydantic import BaseModel, Field, field_validator, EmailStr
3from typing import Annotated
4
5app = FastAPI()
6
7# ── Basic model ────────────────────────────────────────
8class Item(BaseModel):
9 name: str = Field(min_length=1, max_length=100)
10 description: str | None = Field(default=None, max_length=500)
11 price: float = Field(gt=0, description="Price must be positive")
12 tax: float | None = None
13 tags: list[str] = []
14
15 model_config = {
16 "json_schema_extra": {
17 "examples": [{"name": "Laptop", "price": 999.99, "tags": ["electronics"]}]
18 }
19 }
20
21# ── Nested model ───────────────────────────────────────
22class Address(BaseModel):
23 street: str
24 city: str
25 country: str = "India"
26
27class UserCreate(BaseModel):
28 username: str = Field(min_length=3, pattern=r"^[a-zA-Z0-9_]+$")
29 email: str
30 password: str = Field(min_length=8)
31 address: Address | None = None
32
33 @field_validator("password")
34 @classmethod
35 def password_strength(cls, v: str) -> str:
36 if not any(c.isupper() for c in v):
37 raise ValueError("Password must have at least one uppercase letter")
38 return v
39
40# ── Routes using models ────────────────────────────────
41@app.post("/items/")
42def create_item(item: Item) -> Item:
43 if item.tax:
44 item.price = item.price + item.tax
45 return item
46
47@app.put("/items/{item_id}")
48def update_item(item_id: int, item: Item):
49 return {"item_id": item_id, **item.model_dump()}
50
51@app.post("/users/", status_code=201)
52def create_user(user: UserCreate):
53 return {"username": user.username, "email": user.email}
← PREV1. Routing & Path ParamsNEXT →3. Response Models & Status Codes