Skip to content

Chapter 4 · Request Body and Pydantic Models ​

Chapter goals: Learn to declare request bodies with Pydantic BaseModel, master nested models, Body(embed), and multiple body parameters.

4.1 Defining Request Bodies with BaseModel ​

When clients submit data to your API, they use a request body (usually JSON). FastAPI requires you to declare it with a Pydantic model:

python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str                 # Required
    description: str | None = None   # Optional, defaults to None
    price: float              # Required
    tax: float | None = None  # Optional


@app.post("/items/")
def create_item(item: Item):
    # item is already a validated Pydantic object with full type safety
    return {"name": item.name, "price_with_tax": item.price + (item.tax or 0)}

After declaration, FastAPI auto-completes six things:

  1. Reads the request body as JSON;
  2. Type coercion (e.g., "45.2" → 45.2);
  3. Data validation — on failure, returns a 422 error pointing to the exact field;
  4. Gives you a fully-typed Item object with IDE autocomplete;
  5. Generates a JSON Schema for the model;
  6. Puts the schema into the OpenAPI docs — /docs shows a request body example.

::: note GET and request bodies Per spec, GET requests carrying a body are undefined behavior. Swagger UI won't show it, and intermediate proxies may drop it. Use POST/PUT/PATCH for submitting data. :::

4.2 Field Defaults and Required Rules ​

The rule is simple: fields with defaults are optional; fields without defaults are required.

python
from pydantic import BaseModel, Field

class Item(BaseModel):
    name: str
    description: str | None = None          # Optional
    price: float = Field(..., gt=0)         # First arg ... (Ellipsis) means required
    tags: list[str] = []                    # Mutable defaults are safe in Pydantic v2 —
                                            # it copies a fresh list per instance

In Pydantic v2, the first positional argument to Field(...) is the default value; passing ... (Ellipsis) means "must be provided." The more idiomatic style is Field(gt=0) with no default — also indicates required.

4.3 Nested Models ​

A model's field can be another model, forming arbitrarily deep JSON structures:

python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


class Product(BaseModel):
    name: str
    images: list[Image]            # List of models
    metadata: dict[str, float]     # Or dict[str, X] with dynamic keys


@app.post("/products/")
def create_product(product: Product):
    # Access nested attributes: product.images[0].url, fully type-safe
    return {"first_image": product.images[0].url if product.images else None}

A client-submitted JSON looks like:

json
{
  "name": "Smartphone",
  "images": [
    {"url": "https://example.com/a.png", "name": "Front view"}
  ],
  "metadata": {"weight_g": 199.5}
}

Any structural error at any level produces a 422 error pinpointing the exact path.

4.4 Body(embed=True): Wrapping a Single Model ​

If a path function has only one Pydantic model parameter, FastAPI defaults to treating the entire request body as that model. If you want the client to send {"item": {...}} instead of a裸 object, use embed:

python
from fastapi import Body, FastAPI
from pydantic import BaseModel
from typing import Annotated

app = FastAPI()


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


@app.put("/items/{item_id}")
def replace_item(item_id: int, item: Annotated[Item, Body(embed=True)]):
    # Expects request body: {"item": {"name": "pen", "price": 3.0}}
    return {"item_id": item_id, **item.model_dump()}

4.5 Multiple Body Parameters ​

When a route needs both path/query params AND a body, or multiple body models, declare them explicitly with Body:

python
from fastapi import Body, FastAPI
from pydantic import BaseModel

app = FastAPI()


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


class Review(BaseModel):
    rating: int
    comment: str


@app.post("/items/{item_id}/review")
def add_review(
    item_id: int,
    item: Item = Body(..., embed=True),    # First body: embedded
    review: Review = Body(...),            # Second body: top-level
):
    return {"item_id": item_id, "item": item, "review": review}

When there are multiple body params, at least one must use embed=True (or be declared with Body(...)) to disambiguate the JSON structure.

4.6 Chapter Summary ​

  • Pydantic BaseModel declares request bodies with full type safety;
  • Default values = optional; no default = required (Field(...) or omit default);
  • Nested models and dict[str, X] are fully supported;
  • Body(embed=True) wraps a single model under a key;
  • Multiple body params require Body(...) declarations to disambiguate.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. What happens when a required Pydantic field is missing from the request body?

2. When should you use Body(embed=True)?

3. How does FastAPI handle multiple Pydantic model parameters in one route?

4. What is the difference between Field(...) with no args and Field(default=None)?

🛠️ Hands-On Exercises ​

  1. Define a Product model with nested Image list and dict[str, float] metadata; create a POST route that returns the first image URL.
  2. Write a route with two body params (e.g., Item and Review); test both with and without embed=True.
  3. Deliberately omit a required field in the request body; inspect the 422 error JSON to see the exact field path reported.

Proceed to Chapter 5: Advanced Validation with Annotated.