FastAPI Request Body and Data Models

Learn how to handle request bodies and create data models in FastAPI using Pydantic

Intermediate 20 min Tutorial 6 steps

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
1

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

info

FastAPI automatically handles JSON parsing and validation for you

tip

Request bodies are typically used with POST, PUT, PATCH, and DELETE requests

Quick Check

Which HTTP methods typically use request bodies?

2

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:

tip

Use Optional[T] for fields that may not be provided in the request

info

Default values are used when a field is not provided in the request

models.py
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?

3

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:

tip

FastAPI automatically generates API documentation based on your models

info

The model parameter is automatically validated and converted to a Python object

main.py
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.

4

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:

tip

You can nest models to any depth to match your data structure

info

Use List[Model] for arrays of objects and Dict[str, Model] for dictionaries with model values

main.py
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.

5

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:

tip

Use Field for simple constraints like min_length, max_length, ge (greater than or equal), etc.

warning

Custom validators should raise ValueError for validation errors

main.py
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.

6

Best Practices for Data Models

Estimated: 5 min

Let's review some best practices when working with data models in FastAPI:

tip

Keep models focused on a single responsibility - don't create overly complex models

tip

Use descriptive field names and provide examples in the Config class

info

Separate input models from output models when they differ significantly

warning

Be careful with sensitive data like passwords - don't include them in output models

success

Use Pydantic's built-in validators when possible before creating custom ones

models.py
# 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?