Skip to main content

FastAPI Request Validation

Introduction

When building APIs, one of the most critical tasks is ensuring that incoming request data is valid before processing it. FastAPI provides powerful, automatic request validation capabilities through its integration with Pydantic. This validation system helps you:

  • Ensure data meets your requirements (type checking, constraints, etc.)
  • Convert incoming JSON data to Python objects automatically
  • Generate clear error messages when validation fails
  • Document your API's expected input format

In this tutorial, we'll explore how FastAPI handles request validation and how you can leverage this feature to build more robust and error-resistant APIs.

Understanding Pydantic Models

At the core of FastAPI's validation system are Pydantic models. These models define the structure and constraints of your data.

Basic Pydantic Model

python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# Define a model for user data
class User(BaseModel):
username: str
email: str
age: int
is_active: bool = True # Default value

# API endpoint using the model
@app.post("/users/")
async def create_user(user: User):
return {"user_data": user.dict()}

When a client sends a POST request to /users/, FastAPI will:

  1. Read the request body as JSON
  2. Convert the data to the User model
  3. Validate all the data types
  4. Make the validated data available in the user parameter

Validation in Action

Let's see what happens when we send different requests:

Valid Request:

json
// POST to /users/
{
"username": "johndoe",
"email": "[email protected]",
"age": 30
}

Response:

json
{
"user_data": {
"username": "johndoe",
"email": "[email protected]",
"age": 30,
"is_active": true
}
}

Invalid Request:

json
// POST to /users/
{
"username": "johndoe",
"email": "[email protected]",
"age": "thirty"
}

Response:

json
{
"detail": [
{
"loc": ["body", "age"],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}

FastAPI automatically returns a 422 Unprocessable Entity status code with details about the validation error.

Advanced Validation with Pydantic

Field Constraints

Pydantic allows you to add constraints to model fields:

python
from pydantic import BaseModel, Field, EmailStr

class User(BaseModel):
username: str = Field(..., min_length=3, max_length=50)
email: EmailStr
age: int = Field(..., ge=18, le=100) # greater than or equal to 18, less than or equal to 100
password: str = Field(..., min_length=8)

The ... in Field(...) means the field is required.

Custom Validators

You can create custom validation rules:

python
from pydantic import BaseModel, Field, validator

class Item(BaseModel):
name: str
price: float = Field(..., gt=0)
quantity: int = Field(..., ge=1)

@validator('name')
def name_must_not_contain_spaces(cls, v):
if ' ' in v:
raise ValueError('must not contain spaces')
return v.title()

# Calculate total price
@property
def total_price(self):
return self.price * self.quantity

Validating Different Types of Parameters

FastAPI can validate different types of parameters:

Path Parameters

python
from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(
item_id: int = Path(..., title="The ID of the item", ge=1)
):
return {"item_id": item_id}

If someone tries to access /items/abc, they'll get a validation error because abc can't be converted to an integer.

Query Parameters

python
from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/items/")
async def read_items(
skip: int = Query(0, ge=0, description="Number of items to skip"),
limit: int = Query(10, ge=1, le=100, description="Max number of items to return")
):
return {"skip": skip, "limit": limit}

Form Data

python
from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)):
return {"username": username}

Headers and Cookies

python
from fastapi import FastAPI, Header, Cookie

app = FastAPI()

@app.get("/items/")
async def read_items(
user_agent: str = Header(...),
session_id: str = Cookie(None)
):
return {"User-Agent": user_agent, "Session ID": session_id}

Real-World Example: Product API

Let's build a more complex example for an e-commerce API:

python
from fastapi import FastAPI, Path, Query, HTTPException
from pydantic import BaseModel, Field, validator
from typing import List, Optional
from enum import Enum

app = FastAPI()

# Enum for product categories
class Category(str, Enum):
electronics = "electronics"
clothing = "clothing"
food = "food"
books = "books"

# Product model
class Product(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
description: str = Field(..., max_length=1000)
price: float = Field(..., gt=0)
category: Category
in_stock: bool = True
tags: List[str] = []

@validator('price')
def check_price_decimal_places(cls, v):
str_v = str(v)
if '.' in str_v and len(str_v.split('.')[1]) > 2:
raise ValueError('price cannot have more than 2 decimal places')
return v

# Request model for product creation
class ProductCreate(Product):
warehouse_id: int = Field(..., ge=1)

# Product database (in-memory for this example)
products_db = []

@app.post("/products/", response_model=Product)
async def create_product(product: ProductCreate):
# Convert to dict and remove warehouse_id before storing
product_dict = product.dict()
warehouse_id = product_dict.pop("warehouse_id")

# In a real app, you'd use the warehouse_id
print(f"Product will be stored in warehouse: {warehouse_id}")

# Add product to database with an ID
product_entry = {"id": len(products_db) + 1, **product_dict}
products_db.append(product_entry)

return product_entry

@app.get("/products/", response_model=List[Product])
async def list_products(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
category: Optional[Category] = None
):
filtered_products = products_db

if category:
filtered_products = [p for p in filtered_products if p["category"] == category]

return filtered_products[skip:skip + limit]

@app.get("/products/{product_id}", response_model=Product)
async def get_product(
product_id: int = Path(..., ge=1, title="The ID of the product to retrieve")
):
for product in products_db:
if product["id"] == product_id:
return product

raise HTTPException(status_code=404, detail="Product not found")

This example shows:

  1. An enum for limiting possible values
  2. Complex models with multiple field types and constraints
  3. Custom validation with validator
  4. Path and query parameter validation
  5. Response model validation
  6. Error handling with HTTP exceptions

Handling Validation Errors

FastAPI automatically handles validation errors, but you can customize this behavior:

python
from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
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=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder({
"detail": exc.errors(),
"body": exc.body,
"message": "Validation error in request data"
}),
)

Summary

FastAPI's request validation system provides:

  1. Automatic type conversion - Convert JSON to Python objects
  2. Validation - Ensure data meets your requirements
  3. Documentation - Auto-document your API's expected input
  4. Error handling - Clear error messages for invalid data

By leveraging Pydantic models, you can define complex validation rules, including:

  • Type checking
  • Value constraints (min/max, regex patterns)
  • Custom validation logic
  • Default values
  • Complex nested structures

These validation capabilities help you build robust APIs that gracefully handle bad input, reducing the need for manual validation and improving your application's reliability.

Additional Resources

Here are some resources to deepen your understanding of FastAPI request validation:

  1. FastAPI Official Documentation on Data Validation
  2. Pydantic Documentation
  3. FastAPI Body - Multiple Parameters

Exercises

  1. Create a FastAPI endpoint that validates a user registration form with these requirements:

    • Username (3-20 characters, alphanumeric only)
    • Email (valid email format)
    • Password (8+ characters, must contain a number and uppercase letter)
    • Age (must be 18+)
  2. Build an API for a blog post with:

    • Title (required, max 100 chars)
    • Content (required, max 5000 chars)
    • Tags (optional list of strings)
    • Published date (optional date, cannot be in the future)
  3. Extend the product API example to include:

    • Image URLs (list of valid URLs)
    • Dimensions (nested model with height, width, depth)
    • Custom validator ensuring product names don't contain profanity


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)