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:
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:
- Reads the request body as JSON;
- Type coercion (e.g.,
"45.2"→45.2); - Data validation — on failure, returns a 422 error pointing to the exact field;
- Gives you a fully-typed
Itemobject with IDE autocomplete; - Generates a JSON Schema for the model;
- 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.
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 instanceIn 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:
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:
{
"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:
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:
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
BaseModeldeclares 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
- Define a
Productmodel with nestedImagelist anddict[str, float]metadata; create a POST route that returns the first image URL. - Write a route with two body params (e.g.,
ItemandReview); test both with and withoutembed=True. - 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.