Detailed Explanation of FastAPI Request Bodies: Defining Complex Data Structures with Pydantic

1. What is a Request Body?

In web development, we often need to handle data sent by clients. In FastAPI, when clients send complex data (e.g., in JSON format) via methods like POST or PUT, this data is placed in the Request Body. Unlike query parameters passed through the URL, the request body is more suitable for structured data (such as user information, forms, etc.).

For example: When a user registers, we need to collect information like name, age, and address. Using query parameters would make the URL lengthy and insecure, so this data is typically placed in the request body.

2. What is Pydantic?

Pydantic is a data validation and parsing library recommended by FastAPI. It allows us to define data structures (e.g., user information, product details), automatically perform type checking, format validation, and convert request body data into Python objects.

In simple terms, Pydantic acts as a “data guardrail,” ensuring that the data you receive matches the expected format and preventing API exceptions caused by data errors.

3. Defining Your First Pydantic Model

First, install Pydantic (FastAPI includes it by default; if not installed, run pip install pydantic). Then, define a data model by inheriting from pydantic.BaseModel.

Example 1: Simple Model (No Nested Structures)

from pydantic import BaseModel

# Define a User model with required fields: name (str) and age (int)
class User(BaseModel):
    name: str  # Mandatory string field
    age: int   # Mandatory integer field

# FastAPI application to receive the User model as the request body
from fastapi import FastAPI

app = FastAPI()

@app.post("/users/")
def create_user(user: User):
    # FastAPI automatically parses the request body into a User object
    return {"message": f"User {user.name} created successfully! Age: {user.age}"}

Testing the API:
Use Postman or curl to send a POST request to http://localhost:8000/users/ with the following JSON request body:

{
    "name": "Xiaoming",
    "age": 20
}

The API will return a success message: {"message": "User Xiaoming created successfully! Age: 20"}.

4. Complex Data Structures: Nested Models

When data contains sub-structures (e.g., a user has address information), you can nest other Pydantic models within the parent model.

Example 2: Nested Model (User + Address)

from pydantic import BaseModel
from typing import Optional  # For optional fields

# Define a sub-model: Address
class Address(BaseModel):
    street: str  # Street name
    city: str    # City name
    zipcode: str = "100000"  # Optional, with a default value

# Define the main model: User (nested Address)
class User(BaseModel):
    name: str
    age: int
    address: Optional[Address] = None  # Optional address (default: None)

@app.post("/users/with-address/")
def create_user_with_address(user: User):
    # Access nested fields: user.address.street
    return {
        "name": user.name,
        "address": user.address.dict() if user.address else "No address"
    }

Testing:
Send a request with address information:

{
    "name": "Xiaohong",
    "age": 22,
    "address": {
        "street": "100 Tech Road",
        "city": "Beijing"
    }
}

The API returns: {"name": "Xiaohong", "address": {"street": "100 Tech Road", "city": "Beijing", "zipcode": "100000"}}.

5. Complex Data Structures: List Types

To receive multiple items of the same type (e.g., a user’s hobbies list), use the List type (import typing.List).

Example 3: List Type (Hobbies List)

from pydantic import BaseModel
from typing import List, Optional

class User(BaseModel):
    name: str
    age: int
    hobbies: List[str] = []  # List of strings (default: empty list)
    extra_info: Optional[dict] = None  # Optional dictionary (e.g., additional details)

@app.post("/users/with-hobbies/")
def create_user_with_hobbies(user: User):
    return {
        "name": user.name,
        "hobbies": user.hobbies,
        "extra_info": user.extra_info if user.extra_info else "No extra info"
    }

Testing:
Send a request with a hobbies list:

{
    "name": "Xiaogang",
    "age": 25,
    "hobbies": ["Basketball", "Coding"],
    "extra_info": {"height": 180, "weight": 70}
}

The API returns: {"name": "Xiaogang", "hobbies": ["Basketball", "Coding"], "extra_info": {"height": 180, "weight": 70}}.

6. Complex Data Structures: Nested Lists

If a list contains other models (e.g., multiple products in an order), use List[ModelName].

Example 4: Nested List (Order Items)

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float

class Order(BaseModel):
    order_id: int
    items: List[Item]  # List of Item objects

@app.post("/orders/")
def create_order(order: Order):
    total = sum(item.price for item in order.items)
    return {
        "order_id": order.order_id,
        "items": [item.dict() for item in order.items],
        "total_price": total
    }

Testing:
Send a request with a nested list:

{
    "order_id": 1001,
    "items": [
        {"name": "Apple", "price": 5.0},
        {"name": "Banana", "price": 3.0}
    ]
}

The API returns the total price: {"order_id": 1001, "items": [{"name": "Apple", "price": 5.0}, {"name": "Banana", "price": 3.0}], "total_price": 8.0}.

7. Data Validation: Auto-Reject Invalid Data

Pydantic automatically checks if data matches the model definition. If there’s a type error (e.g., age as a string), FastAPI returns a 422 Validation Error.

Example 5: Test Invalid Data
Send a request with an invalid age (string instead of integer):

{
    "name": "Xiaoli",
    "age": "20",  # Error: string instead of integer
    "hobbies": ["Painting"]
}

The API returns an error:

{
    "detail": [
        {
            "loc": ["body", "age"],
            "msg": "Input is not an integer",
            "type": "type_error.integer"
        }
    ]
}

8. Summary

  • Request Body: Suitable for complex data (e.g., JSON) sent via POST/PUT.
  • Pydantic: Defines data structures, auto-validates data, and reduces manual parsing code.
  • Nested Models: Use a ModelName as a field type to define sub-structures (e.g., User + Address).
  • Lists/Dictionaries: Support List and dict types to handle multiple items.
  • Auto-Validation: FastAPI automatically rejects invalid requests and returns clear error messages.

Mastering Pydantic is key to handling complex request bodies in FastAPI, ensuring your API is standardized and robust.

(Note: All code can run directly in a FastAPI project. Install dependencies with pip install fastapi uvicorn, then run with uvicorn main:app --reload.)

Xiaoye