FastAPI Request Body and Data Models
Learn how to handle request bodies and create data models in FastAPI using Pydantic
Overview
Handling request bodies in web APIs requires proper validation and serialization. FastAPI uses Pydantic models to define data structures that automatically validate incoming data and convert it to Python objects.
What You'll Learn:
- Create Pydantic models for request body validation
- Use models in FastAPI endpoints
- Handle nested models and complex data structures
- Customize validation and error messages
- Implement best practices for data modeling in FastAPI
Introduction to Request Bodies in FastAPI
Estimated: 5 min
In FastAPI, request bodies are the data sent by the client to your API, typically in JSON format. FastAPI uses Pydantic models to define the expected structure of this data, providing automatic validation and conversion to Python objects. When a client sends a request with a body, FastAPI will: 1. Read the body of the request as JSON 2. Convert the JSON to Python data types 3. Validate the data against your Pydantic model 4. If validation passes, provide the data as a Python object to your endpoint function 5. If validation fails, return a helpful error message to the client
FastAPI automatically handles JSON parsing and validation for you
Request bodies are typically used with POST, PUT, PATCH, and DELETE requests
Quick Check
Which HTTP methods typically use request bodies?
Creating Basic Pydantic Models
Estimated: 8 min
Pydantic models are Python classes that inherit from BaseModel. They define the structure of your data using type hints. Let's create a simple model for a user profile:
Use Optional[T] for fields that may not be provided in the request
Default values are used when a field is not provided in the request
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
class UserProfile(BaseModel):
username: str
email: str
full_name: Optional[str] = None
age: int
is_active: bool = True
created_at: datetime
class Config:
schema_extra = {
"example": {
"username": "johndoe",
"email": "[email protected]",
"full_name": "John Doe",
"age": 30,
"is_active": True,
"created_at": "2023-01-01T12:00:00"
}
}
This UserProfile model defines the structure of user data. Each field has a type hint that Pydantic uses for validation. Optional fields can be None, and default values are provided for some fields. The Config class allows us to add extra metadata like an example.
Quick Check
What does Optional[str] mean in a Pydantic model?
Using Models in FastAPI Endpoints
Estimated: 7 min
Now let's use our UserProfile model in a FastAPI endpoint. We'll create a simple API that accepts user data and returns it:
FastAPI automatically generates API documentation based on your models
The model parameter is automatically validated and converted to a Python object
from fastapi import FastAPI
from datetime import datetime
from models import UserProfile
app = FastAPI()
@app.post("/users/")
async def create_user(user: UserProfile):
# The user parameter is automatically validated and converted to a UserProfile object
return {"message": "User created successfully", "user": user}
@app.get("/users/{user_id}")
async def read_user(user_id: int):
# This is just an example - in a real app you'd fetch from a database
return {"user_id": user_id, "username": "example_user"}
In the create_user endpoint, we declare the user parameter as type UserProfile. FastAPI will automatically validate the request body against this model and convert it to a UserProfile object. If validation fails, FastAPI returns a 422 error with details about what's wrong.
Working with Nested Models
Estimated: 6 min
Pydantic models can contain other models, allowing you to create complex nested data structures. Let's extend our example to include an address model:
You can nest models to any depth to match your data structure
Use List[Model] for arrays of objects and Dict[str, Model] for dictionaries with model values
from fastapi import FastAPI
from models import UserProfile, Address
app = FastAPI()
@app.post("/users/")
async def create_user(user: UserProfile):
# We can access nested data using dot notation
primary_address = next((addr for addr in user.addresses if addr.is_primary), None)
return {
"message": "User created successfully",
"username": user.username,
"primary_address": primary_address
}
In this endpoint, we access the nested address data using dot notation. We find the primary address and include it in the response.
Custom Validation and Error Handling
Estimated: 7 min
Pydantic provides several ways to add custom validation to your models. Let's explore some common techniques:
Use Field for simple constraints like min_length, max_length, ge (greater than or equal), etc.
Custom validators should raise ValueError for validation errors
from fastapi import FastAPI, HTTPException, status
from models import UserProfile
from pydantic import ValidationError
app = FastAPI()
@app.post("/users/")
async def create_user(user: UserProfile):
try:
# The model is already validated by FastAPI, but we can add additional checks
if user.username == "admin":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username 'admin' is not allowed"
)
return {"message": "User created successfully", "username": user.username}
except ValidationError as e:
# This would catch validation errors if we were manually validating
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=str(e)
)
In this endpoint, we show how to handle validation errors and add custom business logic validation. FastAPI automatically handles Pydantic validation errors, but you can catch them if needed.
Best Practices for Data Models
Estimated: 5 min
Let's review some best practices when working with data models in FastAPI:
Keep models focused on a single responsibility - don't create overly complex models
Use descriptive field names and provide examples in the Config class
Separate input models from output models when they differ significantly
Be careful with sensitive data like passwords - don't include them in output models
Use Pydantic's built-in validators when possible before creating custom ones
# Input model for creating a user
class UserCreate(BaseModel):
username: str
email: str
password: str
age: int
class Config:
schema_extra = {
"example": {
"username": "johndoe",
"email": "[email protected]",
"password": "securepassword123",
"age": 30
}
}
# Output model for returning user data (without password)
class UserResponse(BaseModel):
id: int
username: str
email: str
age: int
is_active: bool
created_at: datetime
class Config:
orm_mode = True # Allows compatibility with ORMs like SQLAlchemy
Here we separate the input model (UserCreate) from the output model (UserResponse). The input model includes a password field, while the output model doesn't. This prevents sensitive data from being exposed in API responses.
Quick Check
Why is it a good practice to separate input models from output models?