FastAPI Request Body
Introduction
When building web APIs, you often need to receive data from clients that is sent as part of the HTTP request. This data could be information for creating new resources, updating existing ones, or any other operation that requires input from the client. In FastAPI, a request body is the mechanism for receiving and processing this data.
A request body is typically sent as JSON in modern web applications, and FastAPI makes it incredibly easy to receive, validate, and work with this data through its powerful data validation features powered by Pydantic.
In this tutorial, you'll learn how to:
- Define request body models
- Validate incoming data
- Work with different types of request data
- Handle optional and required fields
- Create complex nested data structures
Understanding Request Bodies
When a client sends data to your API, it's typically included in the body of the request, especially for HTTP methods like POST, PUT, and PATCH. FastAPI uses Pydantic models to define the structure of these request bodies, which gives you automatic validation, documentation, and type conversion.
Basic Request Body Example
Let's start with a simple example:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# Define the data model for the request
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
@app.post("/items/")
async def create_item(item: Item):
# The incoming request body will be validated against the Item model
# and converted to an Item instance named 'item'
item_dict = item.dict()
if item.tax:
price_with_tax = item.price + item.tax
item_dict.update({"price_with_tax": price_with_tax})
return item_dict
In this example:
- We define a Pydantic model
Item
with four fields - The
name
andprice
fields are required - The
description
andtax
fields are optional (they have default values ofNone
) - FastAPI will automatically validate the incoming JSON against this model
Sample Request
POST /items/
{
"name": "Hammer",
"description": "A sturdy hammer for all your hammering needs",
"price": 19.99,
"tax": 2.00
}
Sample Response
{
"name": "Hammer",
"description": "A sturdy hammer for all your hammering needs",
"price": 19.99,
"tax": 2.00,
"price_with_tax": 21.99
}
Request Body Validation
One of the biggest advantages of using FastAPI is automatic data validation. When you define your model with type annotations, FastAPI will:
- Validate that the required fields are present
- Check that the data types match what's expected
- Convert data when possible (e.g., strings to numbers)
- Return clear error messages when validation fails
Advanced Validation
You can add more complex validation using Pydantic's validation features:
from fastapi import FastAPI
from pydantic import BaseModel, Field, validator
app = FastAPI()
class Product(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
price: float = Field(..., gt=0)
is_in_stock: bool = True
tags: list[str] = []
@validator('name')
def name_must_not_contain_special_chars(cls, v):
if '@' in v or '#' in v:
raise ValueError('name cannot contain @ or # characters')
return v
@app.post("/products/")
async def create_product(product: Product):
return product
In this example:
name
must be between 1 and 50 charactersprice
must be greater than 0is_in_stock
defaults toTrue
- We use a custom validator to ensure the name doesn't contain special characters
Nested Models
Real-world APIs often deal with complex, nested data structures. FastAPI handles these elegantly:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
app = FastAPI()
class Address(BaseModel):
street: str
city: str
country: str
postal_code: str
class User(BaseModel):
username: str
email: str
full_name: str
addresses: List[Address]
@app.post("/users/")
async def create_user(user: User):
return user
Sample Request with Nested Data
POST /users/
{
"username": "johndoe",
"email": "[email protected]",
"full_name": "John Doe",
"addresses": [
{
"street": "123 Main St",
"city": "Anytown",
"country": "USA",
"postal_code": "12345"
},
{
"street": "456 Side St",
"city": "Othertown",
"country": "USA",
"postal_code": "67890"
}
]
}
Handling Different Data Types in Request Bodies
FastAPI can handle various data types in your models:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Dict, Optional
from datetime import datetime, date
app = FastAPI()
class ComplexItem(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: List[str] = []
created_at: datetime
expiry_date: date
metadata: Dict[str, str] = {}
@app.post("/complex-items/")
async def create_complex_item(item: ComplexItem):
return item
In this example, our model includes:
- Lists (
tags
) - Dictionaries (
metadata
) - Date and time fields (
created_at
,expiry_date
) - Optional fields with the
Optional
type hint
Combining Path Parameters and Request Body
You can combine path parameters with request body parameters in a single endpoint:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
return {"item_id": item_id, **item.dict()}
Sample Request
PUT /items/5
{
"name": "Screwdriver",
"price": 9.99,
"description": "A phillips head screwdriver"
}
Sample Response
{
"item_id": 5,
"name": "Screwdriver",
"description": "A phillips head screwdriver",
"price": 9.99,
"tax": null
}
Request Body + Query Parameters
You can also combine query parameters with a request body:
from fastapi import FastAPI, Query
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
@app.post("/items/")
async def create_item(item: Item, q: str = Query(None, max_length=50)):
result = {"item": item.dict()}
if q:
result.update({"q": q})
return result
This endpoint accepts both a request body and an optional query parameter q
.
Real-World Example: User Registration API
Let's look at a more complete example of a user registration API that demonstrates many of these concepts together:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, EmailStr, validator
from typing import Optional
import re
app = FastAPI()
# Database simulation
fake_users_db = {}
# User registration model
class UserRegistration(BaseModel):
username: str = Field(..., min_length=3, max_length=50)
email: EmailStr
password: str = Field(..., min_length=8)
full_name: Optional[str] = None
age: Optional[int] = Field(None, ge=18)
@validator('username')
def username_alphanumeric(cls, v):
if not re.match(r'^[a-zA-Z0-9_]+$', v):
raise ValueError('username must be alphanumeric')
return v
@validator('password')
def password_strength(cls, v):
if not any(char.isdigit() for char in v):
raise ValueError('password must contain at least one digit')
if not any(char.isalpha() for char in v):
raise ValueError('password must contain at least one letter')
return v
# Response model (don't return the password)
class User(BaseModel):
username: str
email: str
full_name: Optional[str] = None
age: Optional[int] = None
@app.post("/register/", response_model=User)
async def register_user(user: UserRegistration):
if user.username in fake_users_db:
raise HTTPException(status_code=400, detail="Username already registered")
# In a real app, you would hash the password here!
fake_users_db[user.username] = user.dict()
# Return user info without the password
return user
Sample Registration Request
POST /register/
{
"username": "johndoe",
"email": "[email protected]",
"password": "secure123",
"full_name": "John Doe",
"age": 25
}
Sample Response
{
"username": "johndoe",
"email": "[email protected]",
"full_name": "John Doe",
"age": 25
}
Summary
FastAPI's approach to handling request bodies provides a powerful way to receive, validate, and process data from clients. By leveraging Pydantic models, you get:
- Automatic validation of incoming data
- Type conversion
- Clear error messages when validation fails
- Self-documenting API with OpenAPI schema generation
- IDE support with autocompletion
The request body feature is one of the core strengths of FastAPI, making it much easier to build robust and type-safe APIs compared to many other Python frameworks.
Additional Resources
- FastAPI Official Documentation on Request Body
- Pydantic Documentation
- JSON Schema - The standard that FastAPI and Pydantic use under the hood
Exercises
-
Create an API endpoint that accepts a shopping cart with items, quantities, and prices, and returns the total cost.
-
Build a blog post submission API that validates a post has a title (5-100 chars), content (min 50 chars), and optional tags.
-
Extend the user registration example to include address information with validation for postal codes.
-
Create an API for a recipe database that validates ingredients as a list of items with quantities and units.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)