Skip to main content

Data Modeling with Pydantic

This document covers FastAPI parameters (Path, Query, Body, Header, Cookie) and data modeling with Pydantic for request/response validation.


📚 Table of Contents​

  1. Part 1: Path Parameters
  2. Part 2: Query Parameters
  3. Part 3: Request Body
  4. Part 4: Data Modeling with Pydantic
  5. Part 5: Header Parameters
  6. Part 6: Cookie Parameters

Part 1: Path Parameters​

What are Path Parameters?​

Definition

Path parameters are variables that are part of the URL path. They allow you to capture values from the URL and use them inside your function.

For example, in the URL /items/42, the value 42 can be captured as a path parameter.

Basic Example​

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}

Explanation:

  • @app.get("/items/{item_id}"): Defines a route where item_id is a path parameter
  • item_id: int: FastAPI automatically converts the path parameter to an integer
  • The function returns the value of item_id as JSON

Multiple Path Parameters​

@app.get("/users/{user_id}/items/{item_id}")
async def read_user_item(user_id: int, item_id: str):
return {"user_id": user_id, "item_id": item_id}

Path Parameter with Validation​

You can use Path() for validation:

from fastapi import Path

@app.get("/products/{product_id}")
async def get_product(
product_id: int = Path(..., gt=0, description="The ID must be greater than 0")
):
return {"product_id": product_id}
  • gt=0: Ensures the ID is greater than 0
  • ...: Means the parameter is required

Path Parameters vs Query Parameters​

TypeLocationExample
PathPart of URL path/items/42
QueryAfter the ?/items?skip=0&limit=10

Part 2: Query Parameters​

1. Basic Query Parameters​

Query parameters are defined as function parameters that are not in the path.

from fastapi import FastAPI

app = FastAPI()

fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]

@app.get("/items/")
async def read_items(skip: int = 0, limit: int = 10):
return fake_items_db[skip : skip + limit]
Example URLValues
http://127.0.0.1:8000/items/skip=0, limit=10
http://127.0.0.1:8000/items/?skip=20skip=20, limit=10

Optional Query Parameters​

To make a query parameter optional, set its default to None:

@app.get("/items/{item_id}")
async def read_item(item_id: str, q: str | None = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}

Boolean Type Conversion​

FastAPI automatically converts strings to booleans. Values like 1, True, true, on, yes are converted to True:

@app.get("/items/{item_id}")
async def read_item(item_id: str, q: str | None = None, short: bool = False):
item = {"item_id": item_id}
if q:
item.update({"q": q})
if not short:
item.update({"description": "This is an amazing item with a long description"})
return item

Required Query Parameters​

Declare a parameter without a default value to make it required:

@app.get("/items/{item_id}")
async def read_user_item(item_id: str, needy: str):
return {"item_id": item_id, "needy": needy}

Mixing Parameter Types​

@app.get("/items/{item_id}")
async def read_user_item(
item_id: str, # Path parameter
needy: str, # Required query parameter
skip: int = 0, # Query with default
limit: int | None = None # Optional query
):
return {"item_id": item_id, "needy": needy, "skip": skip, "limit": limit}

2. Query Parameter Validation​

FastAPI validates query parameters using type annotations and Query():

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/search/")
def search_items(
keyword: str = Query(..., min_length=3, max_length=50, regex="^[a-zA-Z0-9 ]+$")
):
return {"keyword": keyword}

Common Validation Examples:​

String Length and Pattern:

keyword: str = Query(..., min_length=3, max_length=50, regex="^[a-zA-Z0-9 ]+$")

Range Validation for Numbers:

@app.get("/ratings/")
def get_ratings(
min_rating: float = Query(0, ge=0, le=5),
max_rating: float = Query(5, ge=0, le=5)
):
return {"min_rating": min_rating, "max_rating": max_rating}

List of Values:

@app.get("/tags/")
def get_items_by_tags(tags: list[str] = Query([])):
return {"tags": tags}
# URL: /tags/?tags=python&tags=fastapi&tags=api

Using Aliases:

@app.get("/items/")
def get_items(sort_by: str = Query("name", alias="sortBy")):
return {"sort_by": sort_by}
# Accepts ?sortBy=price but maps to sort_by in function

Deprecating a Parameter:

@app.get("/legacy/")
def legacy_endpoint(old_param: str = Query(None, deprecated=True)):
return {"old_param": old_param}

3. Query Parameters with Models​

Group multiple query parameters using a Pydantic model for cleaner code:

Without a Model:

@app.get("/items/")
def get_items(skip: int = 0, limit: int = 10, search: str = None):
return {"skip": skip, "limit": limit, "search": search}

With a Query Parameter Model:

from fastapi import FastAPI, Depends
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

class ItemQueryParams(BaseModel):
skip: int = 0
limit: int = 10
search: Optional[str] = None

@app.get("/items/")
def get_items(params: ItemQueryParams = Depends()):
return params
Benefits of Query Parameter Models
  • Keeps code cleaner and more maintainable
  • Enables reusability of validation logic
  • Automatically generates OpenAPI documentation
  • Supports default values, type checking, and validation rules

Part 3: Request Body​

1. Body Basics​

Definition

A request body is used when the client sends structured data to the server, typically in JSON format for creating or updating resources.

Basic Example Using Body:

from fastapi import FastAPI, Body

app = FastAPI()

@app.post("/users/")
def create_user(name: str = Body(...), age: int = Body(...)):
return {"name": name, "age": age}

Preferred Way - Use a Pydantic Model:

from pydantic import BaseModel

class User(BaseModel):
name: str
age: int

@app.post("/users/")
def create_user(user: User):
return user

FastAPI automatically:

  • Parses the JSON body into a User object
  • Validates the fields
  • Generates OpenAPI docs

Advanced Body Options:

from fastapi import Body

@app.post("/items/")
def create_item(
name: str = Body(..., embed=True, description="The name of the item", example="Book")
):
return {"name": name}
  • embed=True: Wraps the value in a key (e.g., {"name": "Book"} instead of just "Book")

2. Nested Models​

Use nested Pydantic models for complex JSON structures:

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List

app = FastAPI()

class Address(BaseModel):
street: str
city: str
zip_code: str

class User(BaseModel):
name: str
age: int
address: Address # Nested model

@app.post("/users/")
def create_user(user: User):
return user

Example JSON Body:

{
"name": "Alice",
"age": 30,
"address": {
"street": "123 Main St",
"city": "Hanoi",
"zip_code": "100000"
}
}

Nested Lists of Models:

class Item(BaseModel):
name: str
quantity: int

class Order(BaseModel):
order_id: str
items: List[Item]

@app.post("/orders/")
def create_order(order: Order):
return order

Example JSON Body:

{
"order_id": "ORD123",
"items": [
{ "name": "Book", "quantity": 2 },
{ "name": "Pen", "quantity": 5 }
]
}
Benefits of Nested Models
  • Clean structure for complex data
  • Automatic validation of nested fields
  • Better documentation in Swagger UI
  • Reusability of models across endpoints

Part 4: Data Modeling with Pydantic​

1. Pydantic BaseModel​

Pydantic is the foundation for data validation in FastAPI:

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class Item(BaseModel):
name: str = Field(..., min_length=3, max_length=50)
description: str | None = None
price: float = Field(..., gt=0)
in_stock: bool = Field(default=True)

@app.post("/items/")
async def create_item(item: Item):
return {"received": item}

What Happens:

  1. FastAPI reads and parses the JSON body
  2. Converts it into a Pydantic model (Item)
  3. Validates all fields
  4. Returns structured JSON automatically

2. Field Validation​

Using Built-in Validation Rules:

from pydantic import BaseModel, Field

class Item(BaseModel):
name: str = Field(..., min_length=3, max_length=50)
price: float = Field(..., gt=0) # greater than 0
quantity: int = Field(default=1, ge=0, le=1000) # 0 <= quantity <= 1000

Custom Validators:

from pydantic import BaseModel, Field, field_validator

class Item(BaseModel):
name: str
price: float

@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
if v is None or v.strip() == "":
raise ValueError("Name is required and cannot be empty.")
if len(v) < 3:
raise ValueError("Name must be at least 3 characters.")
if len(v) > 50:
raise ValueError("Name must be at most 50 characters.")
return v

@field_validator("price")
@classmethod
def validate_price(cls, v: float) -> float:
if v <= 0:
raise ValueError("Price must be greater than 0.")
return v

3. Response Models​

Use response_model to control output format:

from fastapi import FastAPI
from pydantic import BaseModel
from datetime import datetime

app = FastAPI()

class Item(BaseModel):
name: str

class ItemResponse(BaseModel):
name: str
date_modified: datetime

@app.post("/items/", response_model=ItemResponse)
async def create_item(item: Item):
return ItemResponse(name=item.name, date_modified=datetime.now())

Response:

{
"name": "Laptop",
"date_modified": "2026-01-07T11:58:56.752533"
}

Global Exception Handling​

Handle validation errors globally:

from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

app = FastAPI()

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={
"success": False,
"error": {
"code": "VALIDATION_ERROR",
"message": str(exc),
},
},
)

Part 5: Header Parameters​

Use Header() to extract values from HTTP request headers:

from fastapi import FastAPI, Header

app = FastAPI()

@app.get("/items/")
def read_items(user_agent: str = Header(None)):
return {"User-Agent": user_agent}

Required Header Example​

@app.get("/secure-data/")
def get_secure_data(api_key: str = Header(...)):
return {"API-Key": api_key}

Header Name Conventions​

FastAPI automatically converts underscore _ to hyphen -:

@app.get("/check/")
def check_header(x_token: str = Header(...)):
return {"X-Token": x_token}
# This reads the X-Token header from the request

Multiple Headers​

@app.get("/info/")
def get_info(
x_token: str = Header(...),
x_request_id: str = Header(None)
):
return {"token": x_token, "request_id": x_request_id}

Use Cookie() to extract values from HTTP cookies:

from fastapi import FastAPI, Cookie

app = FastAPI()

@app.get("/read-cookie/")
def read_cookie(session_id: str = Cookie(None)):
return {"session_id": session_id}
@app.get("/secure/")
def secure_area(auth_token: str = Cookie(...)):
return {"auth_token": auth_token}

Session Authentication with Cookies​

from fastapi import FastAPI, Response, Cookie, HTTPException

app = FastAPI()

fake_session_store = {}

@app.post("/login")
def login(response: Response, username: str, password: str):
if username == "admin" and password == "secret":
session_id = "abc123" # In real apps, generate securely
fake_session_store[session_id] = username
response.set_cookie(key="session_id", value=session_id, httponly=True)
return {"message": "Logged in"}
raise HTTPException(status_code=401, detail="Invalid credentials")

@app.get("/profile")
def get_profile(session_id: str = Cookie(None)):
if session_id in fake_session_store:
return {"username": fake_session_store[session_id]}
raise HTTPException(status_code=401, detail="Invalid or missing session")

@app.post("/logout")
def logout(response: Response, session_id: str = Cookie(None)):
if session_id in fake_session_store:
del fake_session_store[session_id]
response.delete_cookie("session_id")
return {"message": "Logged out"}
Key Points
  • response.set_cookie(...): Sets the cookie on login
  • Cookie(...): Reads the cookie from incoming requests
  • response.delete_cookie(...): Removes the cookie on logout
  • httponly=True: Prevents JavaScript access to the cookie (for security)

📊 Parameter Types Summary​

Parameter TypeSourceUse CaseExample
PathURL pathResource identification/users/{id}
QueryURL query stringFiltering, pagination?search=abc&limit=10
BodyRequest body (JSON)Structured data{ "name": "Alice" }
HeaderHTTP headersMetadata, auth, tracingAuthorization: Bearer ...
CookieHTTP cookiesSessions, preferencessession_id=abc123

📖 References​

ResourceLinkDuration
FastAPI Pydantic BaseModelBody in FastAPI~10 min
Nested ModelsBody - Nested Models~10 min
Response ModelResponse Model~10 min
Path ParametersPath Parameters~15 min
Query ParametersQuery Parameters~15 min

Learning Checklist:​

  • Define a Pydantic BaseModel for request body and verify type handling
  • Launch /docs and inspect the generated OpenAPI schemas
  • Create a nested BaseModel structure and test nested validation
  • Add constraints using Field(), Path(), or Query() and verify in OpenAPI UI
  • Use response_model to ensure output filtering and correct documentation
  • Set status_code, description in route decorators for enhanced metadata
  • Test error responses resulting from schema violations
  • Confirm that examples appear in both Swagger UI and ReDoc

Summary

This document covers all input parameter types in FastAPI:

  1. Path Parameters: Part of URL, for resource identification
  2. Query Parameters: After ?, for filtering and pagination
  3. Body: JSON data for complex requests
  4. Header: HTTP headers for metadata and auth
  5. Cookie: Client-side storage for sessions

Pydantic provides powerful validation through BaseModel, Field(), and custom validators.