第 4 章 · 请求体与 Pydantic 模型
本章目标:学会用 Pydantic BaseModel 声明请求体,掌握嵌套模型、Body(embed)、多重 body 参数的用法与 FastAPI 的参数识别体系。
4.1 用 BaseModel 定义请求体
客户端向 API 提交数据时用请求体(通常是 JSON)。FastAPI 要求用 Pydantic 模型声明它:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str # 必填
description: str | None = None # 可选,默认 None
price: float # 必填
tax: float | None = None # 可选
@app.post("/items/")
def create_item(item: Item):
# item 已经是校验过的 Pydantic 对象,属性带完整类型
return {"name": item.name, "price_with_tax": item.price + (item.tax or 0)}声明之后 FastAPI 自动完成六件事:
- 把请求体当 JSON 读取;
- 类型转换(如
"45.2"→45.2); - 数据校验,失败返回指明字段位置的 422 错误;
- 给你一个有编辑器补全的
Item对象; - 为模型生成 JSON Schema;
- schema 进入 OpenAPI 文档,/docs 页面展示出请求体示例。
::: note GET 与请求体 规范上 GET 携带请求体是未定义行为,Swagger UI 不显示、中间代理可能丢弃。提交数据请使用 POST/PUT/PATCH。 :::
4.2 字段默认值与必填规则
规则很简单:有默认值的字段可不传,没有默认值的字段必传。
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str
description: str | None = None # 可不传
price: float = Field(..., gt=0) # Field 的第一个参数 ... 表示必填(Ellipsis)
tags: list[str] = [] # 可变默认值在 Pydantic 中是安全的,
# 它会为每个实例复制一份,不会共享同一个 listPydantic v2 中 Field(...) 的第一个位置参数就是默认值,传 ...(Ellipsis)代表"必须提供";更推荐的写法是 Field(gt=0) 不给默认值,同样表示必填。
4.3 嵌套模型
模型的字段可以是另一个模型,形成任意深度的 JSON 结构:
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] # 模型列表
metadata: dict[str, float] # 也可以是 dict[str, X] 形式的动态键
@app.post("/products/")
def create_product(product: Product):
# 访问嵌套属性:product.images[0].url,全程类型安全
return {"first_image": product.images[0].url if product.images else None}客户端提交的 JSON 长这样:
{
"name": "手机",
"images": [
{"url": "https://example.com/a.png", "name": "正面图"}
],
"metadata": {"weight_g": 199.5}
}任何一层结构错误都会得到指出精确路径的 422 报错。
4.4 Body(embed=True):给单模型包一层 key
如果路径函数只有一个 Pydantic 模型参数,FastAPI 默认把整个请求体当作该模型本身。若希望客户端发 {"item": {...}} 而不是裸对象,用 embed:
from fastapi import Body, FastAPI
from pydantic import BaseModel
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)]):
# 期望请求体: {"item": {"name": "pen", "price": 3.0}}
return {"item_id": item_id, **item.model_dump()}Body(embed=True) 放在 Annotated 里是官方推荐写法(第 5 章系统讲 Annotated)。
4.5 多重 body 参数与 singular 值
可以有多个模型参数,甚至混入标量——FastAPI 会自动分组:
from fastapi import Body, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
class User(BaseModel):
username: str
@app.put("/offers/{offer_id}")
def create_offer(
offer_id: int,
item: Item, # → body["item"]
user: User, # → body["user"]
priority: Annotated[int, Body()], # 标量进 body:→ body["priority"]
):
# 有多个 body 值时,每个都按参数名嵌入:
# {"item": {...}, "user": {...}, "priority": 3}
return {"offer_id": offer_id, "priority": priority}
@app.put("/legacy/{item_id}")
def legacy_update(item_id: int, price: Annotated[float, Body(embed=True)]):
# 单个标量想放 body 必须 Body(embed=True):
# {"price": 9.9}
return {"item_id": item_id, "price": price}识别规则全景
FastAPI 判断函数参数来源的优先级:① 在路径模板中 → path;② 是 Pydantic 模型 → body(多个时按键嵌入);③ 是标量 → query;④ 显式指定 Body()/Query()/Path() 则以显式为准。记住这四条就能解释一切行为。
4.6 常用特殊类型
Pydantic v2 提供开箱即用的语义类型,比裸 str 安全得多:
from datetime import datetime, timedelta
from uuid import UUID
from pydantic import BaseModel, EmailStr, HttpUrl
from fastapi import FastAPI
app = FastAPI()
class Signup(BaseModel):
email: EmailStr # 校验邮箱格式,需要 pip install email-validator
homepage: HttpUrl # 校验 URL 合法性
created_at: datetime # 自动解析 ISO8601 字符串
request_id: UUID # 自动校验 UUID 格式
@app.post("/signup")
def signup(data: Signup):
return {
"email_domain": data.email.split("@")[1],
"normalized_url": str(data.homepage),
}EmailStr 需要额外安装:pip install email-validator(装了 fastapi[standard] 已自带)。
4.7 本章小结
- 请求体用
BaseModel子类声明:有默认值可选、无默认值必填; - 模型可任意嵌套(
list[Image]、dict[str, float]),错误定位到具体路径; - 单模型默认吃掉整个 body;要包 key 用
Annotated[Item, Body(embed=True)]; - 多模型 + 标量可共存于一个 body,各自按参数名嵌入;单个标量进 body 也必须 embed;
- 识别优先级:path > 模型(body) > 标量(query),显式
Body/Query/Path最优先; - 善用
EmailStr/HttpUrl/datetime/UUID等特殊类型替代裸字符串。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 路径函数只有一个参数 item: Item(无其他 body 参数),客户端应如何构造请求体?
2. Pydantic 模型中 description: str | None = None 表示?
3. 函数签名 def f(a: Item, b: User) 时,请求体应该长什么样?
4. 想让标量参数 priority 出现在请求体而不是查询串里,正确写法是?
🛠️ 动手实践
- 设计一个「订单」模型:包含
items: list[OrderItem](嵌套)、coupon_code: str | None、created_at: datetime,实现创建接口并用 /docs 测试非法嵌套数据的报错信息。 - 分别写出同一接口的裸对象版和
embed=True版本,用 curl 发两种请求对比结果。 - 实现一个同时接收
user: User、item: Item和标量note: Annotated[str, Body()]的接口,观察 /docs 生成的请求体示例结构。
会收数据了,下一章进入第 5 章把校验做严。