FastAPI 请求体(Request Body)完全指南:用 Pydantic 模型声明与校验 POST/PUT 数据
本指南以 FastAPI 官方教程「请求体(Request Body)」为核心,系统讲解如何用 Pydantic 模型接收客户端(如浏览器、移动端)提交的 JSON 数据,涵盖模型定义、必填/可选字段声明、与路径参数、查询参数的混用规则,并结合仓库内 docs_src/body/ 下的示例源码与 tests/test_tutorial/test_body/ 中的测试用例,向你展示 FastAPI 如何把「类型声明」变成「JSON 解析 + 数据校验 + OpenAPI 文档」一整条自动化链路。读完本文,你将能独立写出健壮的 POST/PUT 接口,并精确理解每个参数最终落在请求的哪个位置。
什么是请求体,何时需要发送
当客户端(比如浏览器或移动 App)需要向你的 API 发送数据时,数据通常以 请求体(request body) 的形式发送。相应地,响应体(response body) 是你的 API 返回给客户端的数据。
需要区分的是:API 几乎总要返回响应体,但客户端并不总是需要发送请求体——有时客户端只是请求某个路径,可能带上几个查询参数,但不会携带 body。因此,FastAPI 教程对请求体的定位是:在“确有必要”时才用它来承载结构化数据。
关于请求方法,官方文档给出了明确提醒:
- 要发送数据,应使用
POST(最常见)、PUT、DELETE或PATCH之一; - 在
GET请求中发送 body,在规范(HTTP 规范)中属于未定义行为。FastAPI 出于对非常复杂/极端场景的兼容性仍然支持它,但这是被强烈不鼓励的做法——Swagger UI 交互式文档不会为GET展示 body 文档,且中间代理(proxy)也很可能不支持这种用法。
声明请求体的方式十分简单:使用 Pydantic 模型——借助 Pydantic 的全部能力与优势来完成类型转换、校验和序列化。
第一步:导入 Pydantic 的 BaseModel
在开始前,先引入必要的依赖。示例代码位于 docs_src/body/tutorial001_py310.py,首先从 pydantic 导入 BaseModel:
from fastapi import FastAPI
from pydantic import BaseModel
这里的 fastapi.FastAPI 用于创建应用实例,而 BaseModel 是你定义数据模型的基类。
第二步:定义数据模型(Data Model)
把数据模型声明为继承自 BaseModel 的类,并使用标准的 Python 类型标注所有属性:
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
必填与可选字段的约定
与声明查询参数时的规则一致:
- 模型属性有默认值 → 该字段非必填;
- 模型属性没有默认值 → 该字段必填;
- 使用
None作为默认值即可让字段可选。
在上面的 Item 模型中,name: str 与 price: float 没有默认值,因此必填;description 与 tax 的默认值为 None,因此是可选的。这个模型对应一个 JSON「对象」(即 Python 的 dict),例如:
{
"name": "Foo",
"description": "An optional description",
"price": 45.2,
"tax": 3.5
}
因为 description 和 tax 可选,下面的 JSON 同样是合法的请求体:
{
"name": "Foo",
"price": 45.2
}
可选项的文本内容,但别选错可选语义
注意区分两种“可选”:
- 用
str | None(Python 3.10+ 联合类型语法)只表达“类型上允许为 None”,它本身并不决定字段是否必填; - 真正让字段“非必填”的是
= None这个默认值。
这一点在下文混用查询参数时会再次印证。
第三步:把模型声明为路径操作函数的参数
在 路径操作(path operation)函数里,把它当作参数声明即可——就像之前声明路径参数和查询参数那样,并把类型标注为你创建的模型 Item:
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item):
return item
只靠这一处 Python 类型声明,FastAPI 就会自动完成:
- 将请求体作为 JSON 读取;
- 转换对应的类型(如有需要,比如把字符串
"50.5"转成float); - 校验数据——若数据非法,会返回清晰明确的错误,精确指出错误位置与错误内容(HTTP 422 与
ValidationError结构); - 把接收到的数据放入参数
item——由于你在函数中把item的类型声明为Item,编辑器会对该参数的全部属性及其类型提供自动补全等支持; - 为模型生成 JSON Schema 定义,这些 Schema 可被项目在其他地方复用;
- 这些 Schema 会成为生成的 OpenAPI Schema 的一部分,并被自动文档 UI 使用。
完整可运行的示例见 docs_src/body/tutorial001_py310.py。
源码级佐证:请求体在 OpenAPI 中如何呈现
仓库中的测试 tests/test_tutorial/test_body/test_tutorial001.py 对生成的 /openapi.json 做了快照断言(test_openapi_schema)。从中可以看到,POST /items/ 的 OpenAPI 定义里出现了:
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
"required": true
}
同时 components.schemas.Item 被生成为 type: object,其中 required 列表恰好是 ["name", "price"](两个无默认值的字段),而 description 与 tax 被描述为 anyOf: [{"type": "string"}, {"type": "null"}] / anyOf: [{"type": "number"}, {"type": "null"}]。这就是“默认值决定必填性、类型标注决定 Schema”最直观的代码证据。
校验失败的真实形态(来自测试断言)
同样是上述测试文件,覆盖了各种异常请求并断言返回 HTTP 422:
| 请求场景 | 结果 |
|---|---|
缺少必填字段(如只传 {"name": "Foo"}) |
422,错误 type: "missing",loc: ["body", "price"] |
传了无法解析为数字的字符串(price: "twenty") |
422,错误 type: "float_parsing" |
请求体为空对象 {} |
422,同时报告 name 与 price 缺失 |
请求体为 null |
422,错误定位到 loc: ["body"] |
| JSON 语法损坏 | 422,错误 type: "json_invalid",并附 ctx.error |
提交表单而非 JSON(application/x-www-form-urlencoded) |
422 |
Content-Type 不是 JSON 类型 |
422 |
反过来,只要 Content-Type: application/json(或 application/geo+json 等 JSON 变体)且数据合法,返回就是 200。另外测试还演示了类型强制转换:price 传字符串 "50.5" 时,响应中会变成浮点数 50.5——这正是“按需转换类型”的直接证据。
在函数中使用模型对象
进入函数内部后,可以直接访问模型对象的各个属性。教程的进阶版示例 docs_src/body/tutorial002_py310.py 展示了更真实的业务用法:先把模型转为 dict,再依据可选字段动态加工数据。
@app.post("/items/")
async def create_item(item: Item):
item_dict = item.model_dump()
if item.tax is not None:
price_with_tax = item.price + item.tax
item_dict.update({"price_with_tax": price_with_tax})
return item_dict
这里的 item.model_dump()(Pydantic v2 中替代旧版 item.dict() 的方法)把模型实例序列化为普通 dict,随后检查可选字段 item.tax 是否真的传入了值,只有非 None 时才计算含税价格并追加键 price_with_tax。
对应的测试 tests/test_tutorial/test_body/test_tutorial002.py 也很有意思:
- 传
tax: 0.3时,响应为{"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3, "price_with_tax": 50.8}——证明price_with_tax由 50.5 + 0.3 计算而来; - 不传
tax时,响应中不含price_with_tax键,tax为None——证明model_dump()保留了可选键为None,而条件分支正确地跳过了计算; - 测试用参数化同时验证了
price传"50.5"字符串与50.5浮点数时都能得到相同的50.5数值结果,再次佐证类型转换能力。
请求体与路径参数同时使用
你完全可以同时声明路径参数与请求体。FastAPI 会智能识别:函数参数中与路径参数同名/匹配的,从路径中取值;被声明为 Pydantic 模型类型的参数,则从请求体中读取。
来看示例 docs_src/body/tutorial003_py310.py:
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
return {"item_id": item_id, **item.model_dump()}
这里 item_id 出现在路径 "/items/{item_id}" 中,因此被当作路径参数解析为 int;item 的类型是 Pydantic 模型,于是被当作请求体解析。返回值把路径中的 item_id 与模型字段合并成一个响应对象。
测试 tests/test_tutorial/test_body/test_tutorial003.py 中用 client.put("/items/123", json={...}) 验证:路径中的 "123" 被转换为整数 123,响应为 {"item_id": 123, ...}。同时该测试生成的 OpenAPI 快照中,路径参数 item_id 位于 parameters(in: "path"、required: true、type: "integer"),而请求体独立出现在 requestBody——这说明二者在文档和解析上完全分离、互不干扰。
请求体 + 路径参数 + 查询参数三者混用
更进一步,你还可以在同一个接口里同时声明请求体、路径参数和查询参数,FastAPI 会为每个参数自动从正确位置取值。示例见 docs_src/body/tutorial004_py310.py:
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, q: str | None = None):
result = {"item_id": item_id, **item.model_dump()}
if q:
result.update({"q": q})
return result
调用 /items/123?q=somequery 并携带 JSON 请求体时,item_id 来自路径、q 来自查询串、item 来自请求体。
FastAPI 的参数识别规则
函数参数会被按如下规则归类(官方文档明确给出):
- 若参数同时声明在路径中 → 作为路径参数;
- 若参数是单一类型(如
int、float、str、bool等)→ 作为查询参数; - 若参数类型被声明为 Pydantic 模型 → 作为请求体。
关于 q: str | None = None 的必填性说明
文档特别提示:FastAPI 判断 q 非必填,依据的是默认值 = None,而不是 str | None 这个类型注解本身。不加默认值的 q: str 就会变成必填查询参数。当然,写出 str | None 这样的类型注解依然有意义——它能让编辑器给出更好的补全与错误提示,这属于“类型正确性”的范畴。
对应的测试 tests/test_tutorial/test_body/test_tutorial004.py 做了两点关键验证:
- 行为上:
client.put("/items/123", json={...}, params={"q": "somequery"})时响应包含"q": "somequery";不带q时响应不含该键。 - 文档上:OpenAPI 快照中
q出现在parameters里,且为"required": false的in: "query"参数;模型Item仍然只在requestBody中被引用。这正好与“默认值None→ 非必填”及“单值类型 → 查询参数”两条规则互相印证。
自动生成的交互式文档
由于模型会被纳入 OpenAPI Schema,自动生成的交互式 API 文档会直接展示你的数据模型,例如:
- 在「Schemas」区域列出模型的 JSON Schema(文档截图见 docs/en/docs/img/tutorial/body/image01.png);
- 在每个使用该模型的路径操作内部,也会内嵌展示该请求体 Schema 与 422 校验错误结构(见 docs/en/docs/img/tutorial/body/image02.png)。
你可以访问应用根路径的 /docs(Swagger UI)直接试发请求体并观察校验响应。
编辑器的类型提示与自动补全支持
使用 Pydantic 模型而非裸 dict 的另一个巨大好处是编辑器支持:
- 在函数体内编写
item.时,编辑器会给出所有属性及其类型的补全与类型提示(截图见 docs/en/docs/img/tutorial/body/image03.png); - 对类型不正确的操作(例如把
item.price(float)当字符串拼接),编辑器会直接标出错误(截图见 docs/en/docs/img/tutorial/body/image04.png)。
这种体验并非巧合:FastAPI 整个框架正是围绕“类型声明驱动”这一设计目标构建的,并且在实现之前就经过了设计阶段的严格测试,以确保能与各家编辑器协同工作,Pydantic 本身也为此做过相应改动。上面的截图来自 Visual Studio Code,但在 PyCharm 及大多数主流 Python 编辑器中也有一致的体验(PyCharm 下的效果见 docs/en/docs/img/tutorial/body/image05.png)。若使用 PyCharm,还可以安装 Pydantic PyCharm Plugin 来获得对 Pydantic 模型更完善的自动补全、类型检查、重构、搜索与代码检查能力。
不使用 Pydantic 的替代方案
如果你不想使用 Pydantic 模型,也可以直接用 Body 参数来接收请求体中的单个值。相关内容属于「请求体 – 多参数」教程的范畴,详见官方文档 docs/en/docs/tutorial/body-multiple-params.md#singular-values-in-body 中「body 中的单值」一节(法语文档入口见 docs/fr/docs/tutorial/body.md 结尾的指引)。
小结
从一次简单的 POST /items/ 到「路径参数 + 查询参数 + 请求体」三合一接口,FastAPI 请求体的核心心智模型只有一句话:类型声明即一切。写对类型、写对默认值,FastAPI 就替你完成了 JSON 读取、类型转换、数据校验、422 错误响应与 OpenAPI 文档生成的全部工作;而 Pydantic 模型带来的编辑器补全与静态检查,则让这类代码从「能跑」走向「可靠、可维护」。想进一步实践,可以运行仓库内 docs_src/body/ 下的四个示例,并用 tests/test_tutorial/test_body/ 目录中的测试用例对照学习各种边界情况(缺字段、坏 JSON、错误 Content-Type、类型强转等)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00