Skip to main content

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:

python
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:

  1. We define a Pydantic model Item with four fields
  2. The name and price fields are required
  3. The description and tax fields are optional (they have default values of None)
  4. FastAPI will automatically validate the incoming JSON against this model

Sample Request

json
POST /items/
{
"name": "Hammer",
"description": "A sturdy hammer for all your hammering needs",
"price": 19.99,
"tax": 2.00
}

Sample Response

json
{
"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:

  1. Validate that the required fields are present
  2. Check that the data types match what's expected
  3. Convert data when possible (e.g., strings to numbers)
  4. Return clear error messages when validation fails

Advanced Validation

You can add more complex validation using Pydantic's validation features:

python
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 characters
  • price must be greater than 0
  • is_in_stock defaults to True
  • 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:

python
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

json
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:

python
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:

python
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

json
PUT /items/5
{
"name": "Screwdriver",
"price": 9.99,
"description": "A phillips head screwdriver"
}

Sample Response

json
{
"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:

python
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:

python
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

json
POST /register/
{
"username": "johndoe",
"email": "[email protected]",
"password": "secure123",
"full_name": "John Doe",
"age": 25
}

Sample Response

json
{
"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

Exercises

  1. Create an API endpoint that accepts a shopping cart with items, quantities, and prices, and returns the total cost.

  2. Build a blog post submission API that validates a post has a title (5-100 chars), content (min 50 chars), and optional tags.

  3. Extend the user registration example to include address information with validation for postal codes.

  4. 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! :)