05. FastAPI 请求体(前后端演示与 model_dump())
请求体到底放在哪里
请求体(Request Body)就是 HTTP 请求的正文。提交一篇文章时,标题、正文和标签是一整份业务数据,适合放在 POST 请求体中,而不是全部拼到 URL 后面。
一次 JSON 请求的简化结构如下。空行后面的内容就是请求体:
POST /articles HTTP/1.1
Content-Type: application/json
{"title":"学习 FastAPI","content":"认识请求体","tags":["Python","Web"]}本篇用一个完整案例串起前后端的数据变化:
浏览器中的 JavaScript 对象
→ JSON.stringify() 转成 JSON 请求体
→ FastAPI 解析并验证请求体
→ 接口收到 Pydantic 模型对象
→ model_dump() 转成 Python 字典完整案例:前端提交文章,后端接收请求体
准备两个文件,并放在同一个目录中:
demo/
├─ main.py
└─ index.html这个案例不连接数据库。后端给文章返回一个固定的模拟 id,目的是专心观察请求体如何进入接口。
后端:main.py
from fastapi import FastAPI
from fastapi.responses import FileResponse
from pydantic import BaseModel
app = FastAPI()
class ArticleCreate(BaseModel):
title: str
content: str
tags: list[str]
@app.get("/")
def read_index():
return FileResponse("index.html")
@app.post("/articles", status_code=201)
def create_article(article: ArticleCreate):
# article 是 ArticleCreate 模型对象,不是字典
data = article.model_dump()
# 转成字典后,可以增加、删除或修改字段
data["id"] = 1
return data关键点是 article: ArticleCreate:
- 它告诉 FastAPI 从请求体读取数据,而不是从查询字符串读取
article。 - FastAPI 按模型检查
title、content和tags,再创建ArticleCreate对象。 - 数据符合要求才会执行
create_article();不符合时通常返回 422。 article.model_dump()把模型对象转换成普通 Python 字典,方便继续加工。
read_index() 只负责返回前端页面,让页面和接口都使用 127.0.0.1:8000,这样演示时不需要额外处理跨域。
前端:index.html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FastAPI 请求体演示</title>
</head>
<body>
<h1>提交文章</h1>
<form id="article-form">
<p>
<label for="title">标题</label><br />
<input id="title" name="title" value="学习 FastAPI" required />
</p>
<p>
<label for="content">正文</label><br />
<textarea id="content" name="content" required>认识请求体</textarea>
</p>
<p>
<label for="tags">标签(用逗号分隔)</label><br />
<input id="tags" name="tags" value="Python, Web" />
</p>
<button type="submit">提交</button>
</form>
<h2>后端响应</h2>
<pre id="result">尚未提交</pre>
<script>
const form = document.querySelector("#article-form");
const result = document.querySelector("#result");
form.addEventListener("submit", async (event) => {
// 阻止表单默认跳转,改由 fetch 发送请求
event.preventDefault();
const formData = new FormData(form);
// 先根据表单内容组装 JavaScript 对象
const payload = {
title: formData.get("title"),
content: formData.get("content"),
tags: formData
.get("tags")
.split(/[,,]/)
.map((tag) => tag.trim())
.filter(Boolean),
};
try {
const response = await fetch("/articles", {
method: "POST",
headers: {
// 告诉后端:请求体是 JSON
"Content-Type": "application/json",
},
// 把 JavaScript 对象转成 JSON 字符串,放进请求体
body: JSON.stringify(payload),
});
const data = await response.json();
if (!response.ok) {
throw new Error(`HTTP ${response.status}:${JSON.stringify(data)}`);
}
result.textContent = JSON.stringify(data, null, 2);
} catch (error) {
result.textContent = `请求失败:${error.message}`;
}
});
</script>
</body>
</html>前端最容易混淆的是 payload 和 body:
payload是 JavaScript 对象,暂时还不是请求体。JSON.stringify(payload)得到 JSON 字符串,例如{"title":"学习 FastAPI","tags":["Python","Web"]}。body: JSON.stringify(payload)才把这个字符串作为请求体发送。Content-Type: application/json说明请求体的格式;它负责声明格式,不会自动转换数据。
运行并观察请求体
在 main.py 和 index.html 所在目录启动:
uv run uvicorn main:app --reload浏览器打开 http://127.0.0.1:8000/,点击“提交”,页面应显示类似结果:
{
"title": "学习 FastAPI",
"content": "认识请求体",
"tags": ["Python", "Web"],
"id": 1
}打开浏览器开发者工具的“网络(Network)”面板,选择这次 articles 请求:
- “负载(Payload)”或“请求”中可以看到前端发出的 JSON 请求体。
- “响应(Response)”中可以看到后端增加
id后返回的 JSON。 - 请求地址仍然是
/articles,文章数据没有出现在 URL 中。
也可以打开 http://127.0.0.1:8000/docs,展开 POST /articles,在 Request body 中修改 JSON 并执行。因为后端声明了 Pydantic 模型,Swagger UI 能自动展示请求体结构。
model_dump() 是什么
model_dump() 是 Pydantic 2 模型的方法,作用是:把模型对象转换成 Python 字典。嵌套的 Pydantic 模型也会递归转换。
article = ArticleCreate(
title="学习 FastAPI",
content="认识请求体",
tags=["Python", "Web"]
)
data = article.model_dump()
print(type(data))
# <class 'dict'>
print(data)
# {
# 'title': '学习 FastAPI',
# 'content': '认识请求体',
# 'tags': ['Python', 'Web']
# }在前面的接口中,FastAPI 已经把请求体转换成 article,所以可以直接使用:
data = article.model_dump()
data["id"] = 1如果直接使用 article["id"] = 1 会报错,因为模型对象不是字典,不能按字典方式赋值。
还要注意:model_dump() 返回的是字典,不是 JSON 字符串。如果需要 JSON 字符串,Pydantic 2 提供的是 model_dump_json()。FastAPI 能自动序列化接口返回的模型或字典,因此仅仅为了 return article,通常不必手动调用 model_dump()。
model_dump() 的常用参数
| 写法 | 作用 |
|---|---|
model_dump() | 默认转成 Python 字典,保留所有字段 |
model_dump(mode="json") | 转成适合 JSON 序列化的字典,例如把 datetime 转成字符串 |
model_dump(exclude_none=True) | 排除值为 None 的字段 |
model_dump(exclude_unset=True) | 排除创建模型时没有显式传入的字段 |
model_dump(exclude_defaults=True) | 排除当前值等于默认值的字段 |
model_dump(include={"title", "tags"}) | 只保留指定字段 |
model_dump(exclude={"content"}) | 排除指定字段 |
model_dump(by_alias=True) | 字段定义了别名时,使用别名作为字典键 |
默认的 mode="python" 会尽量保留 Python 类型;mode="json" 则生成 JSON 兼容值。两种模式返回的都是字典,区别主要在值的类型。
只保留或排除字段
article.model_dump(include={"title", "tags"})
# {'title': '学习 FastAPI', 'tags': ['Python', 'Web']}
article.model_dump(exclude={"content"})
# {'title': '学习 FastAPI', 'tags': ['Python', 'Web']}exclude_unset 与 exclude_none 的区别
这两个参数在 PATCH 局部更新中特别有用。先定义一个所有字段都可省略的更新模型:
class ArticleUpdate(BaseModel):
title: str | None = None
content: str | None = None只传 title 时:
changes = ArticleUpdate(title="新标题")
changes.model_dump()
# {'title': '新标题', 'content': None}
changes.model_dump(exclude_unset=True)
# {'title': '新标题'}exclude_unset=True 只保留调用方显式传入的字段,因此适合生成“本次需要更新哪些列”的字典。
如果调用方显式传入 content: null,情况又不同:
changes = ArticleUpdate(title="新标题", content=None)
changes.model_dump(exclude_unset=True)
# {'title': '新标题', 'content': None}
changes.model_dump(exclude_none=True)
# {'title': '新标题'}exclude_unset=True:content被显式传入了,所以保留,值仍为None。exclude_none=True:只要值是None就排除,即使调用方显式传入。
因此,如果业务中 null 表示“清空正文”,不能随意用 exclude_none=True,否则这个更新意图会被丢掉。
一个对应的 PATCH 接口可以这样写:
@app.patch("/articles/{article_id}")
def update_article(article_id: int, changes: ArticleUpdate):
update_data = changes.model_dump(exclude_unset=True)
# 实际项目中在这里按 update_data 更新数据库
return {"id": article_id, "changes": update_data}不定义模型:直接读取原始 JSON
如果只想观察原始数据,暂时不需要类型验证,也可以通过 Request 读取请求体:
from fastapi import FastAPI, Request
app = FastAPI()
@app.post("/echo")
async def echo(request: Request):
data = await request.json()
return {"received": data}这里使用 async def,因为读取请求体时使用了 await。request.json() 返回的是解析后的 Python 值:JSON 对象对应 dict,数组对应 list,还可能是字符串、数字、布尔值或 None。
这种方式不检查字段,也不会根据请求体自动生成模型文档。JSON 语法损坏时,也不会自动获得前面 Pydantic 示例中的字段级 422 验证结果。
另外,data 本身已经是字典或其他 Python 值,不是 Pydantic 模型,不能调用 data.model_dump()。如果 data 是字典,直接使用即可。
JSON 与 Python 值的对应关系
| 概念 | JSON 文本 | Python 值 |
|---|---|---|
| 空值 | null | None |
| 真、假 | true、false | True、False |
| 对象 | {"name":"张三"} | dict |
| 数组 | [1, 2] | list |
JSON 对象使用双引号,不支持注释和末尾多余逗号。{"name": "张三"} 是 JSON;{'name': '张三'} 是 Python 字典的常见写法,不是合法 JSON 文本。
常见问题与练习
- 后端收到 422:先检查请求体是否为合法 JSON,再看是否缺字段、字段名是否写错、值能否转换成模型声明的类型。
- 前端只写
body: payload:fetch()不会按这里的需求自动发送 JSON 对象,应使用JSON.stringify(payload)并声明Content-Type。 - 把查询参数当成请求体:
POST /articles?title=xxx中的title在查询字符串中,不在请求体中。 - 对字典调用
model_dump():只有 Pydantic 模型实例才有这个方法,普通字典不需要也不能调用它。 - 为了返回响应而手动转换:FastAPI 可以直接返回 Pydantic 模型;需要加工数据、筛选字段或传给只接受字典的代码时,再使用
model_dump()。
练习:先正常运行前后端案例,再在开发者工具中修改请求体,分别测试缺少 title、把 tags 改成普通字符串、发送损坏的 JSON。最后给 ArticleCreate 增加 note: str | None = None,比较 model_dump()、model_dump(exclude_none=True) 和 model_dump(exclude_unset=True) 的输出。