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
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:
- Read the request body as JSON
- Convert the data to the
User
model - Validate all the data types
- Make the validated data available in the
user
parameter
Validation in Action
Let's see what happens when we send different requests:
Valid Request:
// POST to /users/
{
"username": "johndoe",
"email": "[email protected]",
"age": 30
}
Response:
{
"user_data": {
"username": "johndoe",
"email": "[email protected]",
"age": 30,
"is_active": true
}
}
Invalid Request:
// POST to /users/
{
"username": "johndoe",
"email": "[email protected]",
"age": "thirty"
}
Response:
{
"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:
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:
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
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
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
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
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:
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:
- An enum for limiting possible values
- Complex models with multiple field types and constraints
- Custom validation with
validator
- Path and query parameter validation
- Response model validation
- Error handling with HTTP exceptions
Handling Validation Errors
FastAPI automatically handles validation errors, but you can customize this behavior:
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:
- Automatic type conversion - Convert JSON to Python objects
- Validation - Ensure data meets your requirements
- Documentation - Auto-document your API's expected input
- 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:
- FastAPI Official Documentation on Data Validation
- Pydantic Documentation
- FastAPI Body - Multiple Parameters
Exercises
-
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+)
-
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)
-
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! :)