Skip to content

第 4 章 · 请求体与 Pydantic 模型

本章目标:学会用 Pydantic BaseModel 声明请求体,掌握嵌套模型、Body(embed)、多重 body 参数的用法与 FastAPI 的参数识别体系。

4.1 用 BaseModel 定义请求体

客户端向 API 提交数据时用请求体(通常是 JSON)。FastAPI 要求用 Pydantic 模型声明它:

python
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 自动完成六件事:

  1. 把请求体当 JSON 读取;
  2. 类型转换(如 "45.2"45.2);
  3. 数据校验,失败返回指明字段位置的 422 错误;
  4. 给你一个有编辑器补全的 Item 对象;
  5. 为模型生成 JSON Schema;
  6. schema 进入 OpenAPI 文档,/docs 页面展示出请求体示例。

::: note GET 与请求体 规范上 GET 携带请求体是未定义行为,Swagger UI 不显示、中间代理可能丢弃。提交数据请使用 POST/PUT/PATCH。 :::

4.2 字段默认值与必填规则

规则很简单:有默认值的字段可不传,没有默认值的字段必传

python
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 中是安全的,
                                            # 它会为每个实例复制一份,不会共享同一个 list

Pydantic v2 中 Field(...) 的第一个位置参数就是默认值,传 ...(Ellipsis)代表"必须提供";更推荐的写法是 Field(gt=0) 不给默认值,同样表示必填。

4.3 嵌套模型

模型的字段可以是另一个模型,形成任意深度的 JSON 结构:

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]            # 模型列表
    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 长这样:

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:

python
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 会自动分组:

python
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 安全得多:

python
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 出现在请求体而不是查询串里,正确写法是?

🛠️ 动手实践

  1. 设计一个「订单」模型:包含 items: list[OrderItem](嵌套)、coupon_code: str | Nonecreated_at: datetime,实现创建接口并用 /docs 测试非法嵌套数据的报错信息。
  2. 分别写出同一接口的裸对象版和 embed=True 版本,用 curl 发两种请求对比结果。
  3. 实现一个同时接收 user: Useritem: Item 和标量 note: Annotated[str, Body()] 的接口,观察 /docs 生成的请求体示例结构。

会收数据了,下一章进入第 5 章把校验做严。