Skip to content

05. FastAPI 请求体(前后端演示与 model_dump()) ​

请求体到底放在哪里 ​

请求体(Request Body)就是 HTTP 请求的正文。提交一篇文章时,标题、正文和标签是一整份业务数据,适合放在 POST 请求体中,而不是全部拼到 URL 后面。

一次 JSON 请求的简化结构如下。空行后面的内容就是请求体:

http
POST /articles HTTP/1.1
Content-Type: application/json

{"title":"学习 FastAPI","content":"认识请求体","tags":["Python","Web"]}

本篇用一个完整案例串起前后端的数据变化:

text
浏览器中的 JavaScript 对象
  → JSON.stringify() 转成 JSON 请求体
  → FastAPI 解析并验证请求体
  → 接口收到 Pydantic 模型对象
  → model_dump() 转成 Python 字典

完整案例:前端提交文章,后端接收请求体 ​

准备两个文件,并放在同一个目录中:

text
demo/
├─ main.py
└─ index.html

这个案例不连接数据库。后端给文章返回一个固定的模拟 id,目的是专心观察请求体如何进入接口。

后端:main.py ​

python
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:

  1. 它告诉 FastAPI 从请求体读取数据,而不是从查询字符串读取 article。
  2. FastAPI 按模型检查 title、content 和 tags,再创建 ArticleCreate 对象。
  3. 数据符合要求才会执行 create_article();不符合时通常返回 422。
  4. article.model_dump() 把模型对象转换成普通 Python 字典,方便继续加工。

read_index() 只负责返回前端页面,让页面和接口都使用 127.0.0.1:8000,这样演示时不需要额外处理跨域。

前端:index.html ​

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 所在目录启动:

powershell
uv run uvicorn main:app --reload

浏览器打开 http://127.0.0.1:8000/,点击“提交”,页面应显示类似结果:

json
{
  "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 模型也会递归转换。

python
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,所以可以直接使用:

python
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 兼容值。两种模式返回的都是字典,区别主要在值的类型。

只保留或排除字段 ​

python
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 局部更新中特别有用。先定义一个所有字段都可省略的更新模型:

python
class ArticleUpdate(BaseModel):
    title: str | None = None
    content: str | None = None

只传 title 时:

python
changes = ArticleUpdate(title="新标题")

changes.model_dump()
# {'title': '新标题', 'content': None}

changes.model_dump(exclude_unset=True)
# {'title': '新标题'}

exclude_unset=True 只保留调用方显式传入的字段,因此适合生成“本次需要更新哪些列”的字典。

如果调用方显式传入 content: null,情况又不同:

python
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 接口可以这样写:

python
@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 读取请求体:

python
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 值
空值nullNone
真、假true、falseTrue、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) 的输出。