首页
/ FastAPI 表单数据(Form Data)处理全指南:使用 Form 声明表单字段、理解 urlencoded 编码与 OAuth2 密码流

FastAPI 表单数据(Form Data)处理全指南:使用 Form 声明表单字段、理解 urlencoded 编码与 OAuth2 密码流

2026-09-07 21:07:53作者:舒璇辛Bertina

本指南以 FastAPI 官方教程「Form Data」(本仓库对应文档为 docs/es/docs/tutorial/request-forms.md 及其英文原版 docs/en/docs/tutorial/request-forms.md)为核心展开,面向需要接收 application/x-www-form-urlencoded 表单数据的开发者,覆盖登录表单、OAuth2 "password flow"、传统 HTML 表单提交等典型场景。读完本文,你将掌握如何用 Form 声明并校验表单字段、理解表单编码与 JSON 的差异、避开 Form 与 JSON Body 混用的坑,并知晓 FastAPI 底层如何解析表单请求。

为什么需要 Form:HTML 表单并不默认发 JSON

前端 fetch/axios 发送 JSON 时,请求体会被编码为 application/json;但 HTML 表单(<form></form>)把数据提交给服务器时,使用的是一套截然不同的"特殊"编码。当表单里只有普通字段(无文件)时,浏览器默认用 application/x-www-form-urlencoded 编码:所有字段以 key=value 的形式出现,用 & 连接、做 URL 转义,例如登录请求体会是:

username=foo&password=secret

FastAPI 内置的请求体解析会先判断 body 字段的类型:普通 Body/Pydantic 模型期望 JSON,而 Form 字段则让框架走另一条解析路径——调用底层 Starlette 的 request.form() 从表单数据中取值(详见下文"源码视角")。也就是说,需要接收表单字段而不是 JSON 时,就声明 Form;FastAPI 会自动从正确的"位置"读取这些数据。

前置准备:安装 python-multipart

表单解析依赖第三方库 python-multipart,因此使用前必须先安装。

官方教程推荐用 uv 将其加入项目:

$ uv add python-multipart

习惯 pip 的用户也可等价的执行 pip install python-multipart。FastAPI 在应用启动阶段就会做检查:声明了 Form 参数但环境中没有该库时,会抛出运行时错误。相关实现位于 fastapi/dependencies/utils.py,其中针对两种典型误装情形分别给出提示:

  • 未安装任何实现:提示 Form data requires "python-multipart" to be installed.
  • 误装了同名的 multipart 包:提示先 pip uninstall multipart,再 pip install python-multipart(检测逻辑会区分 python_multipart 与错误的 multipart 实现)。

作为佐证,当前仓库的 pyproject.toml 在项目依赖中声明了 python-multipart >=0.0.18(在 docs、测试等依赖分组中也同样声明,见同文件 86、104 行),说明该库是官方约定俗成的表单/文件解析依赖。

第一步:导入 Form

fastapi 直接导入 Form

from typing import Annotated

from fastapi import FastAPI, Form

app = FastAPI()

对应本仓库的完整示例代码见 docs_src/request_forms/tutorial001_an_py310.py(其中 from fastapi import FastAPI, Form 正是导入 Form 的一行)。

第二步:像声明 Body/Query 一样声明 Form 参数

声明 Form 参数的方式与 BodyQuery 几乎一致,使用 Annotated 元数据:

@app.post("/login/")
async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]):
    return {"username": username}

仓库还保留了不使用 Annotated 的等价写法(docs_src/request_forms/tutorial001_py310.py),用默认值方式声明:

@app.post("/login/")
async def login(username: str = Form(), password: str = Form()):
    return {"username": username}

两种写法完全等价,测试也同时覆盖二者(见下文测试章节)。启动该应用后,可用 curl 直接验证表单提交:

$ curl -X POST http://127.0.0.1:8000/login/ \
    -d "username=foo&password=secret"
{"username":"foo"}

OAuth2 密码流(password flow)中的应用

教程特别指出一个经典场景:OAuth2 规范中被称为 "password flow" 的授权方式,要求客户端把 usernamepassword 作为表单字段发送(而非 JSON),且字段名必须严格是 usernamepassword。上面这个登录端点就是其最简形态——后续扩展为 /token 并把两个字段喂给 OAuth2PasswordRequestForm 即可搭建真实的密码流认证。而 FastAPI 的 /docs 交互界面(Swagger UI)对表单类型请求体也有原生支持,可以直接点 "Try it out" 填写 username/password 后发送测试。

Form 与 Body 的关系:为什么必须显式用 Form

FastAPI 规定:声明表单 body 时必须显式使用 Form。若不声明,参数会被默认解释为查询参数(标量类型)或 JSON body 参数(复杂类型)。这一默认推断逻辑可以追溯到 fastapi/dependencies/utils.py:当参数既没有显式 field_info、也不是路径/文件等特殊场景时,框架会按"是否标量类型"决定把它当成 Query 还是 Body——只有你显式写了 Form(),才会被归入表单参数。

文档还强调了一个类型层面的事实:Form 是直接继承自 Body 的类。打开 fastapi/params.py 可以看到 class Form(Body) 的定义,因此 Form 天然继承了 Body 的整套参数化能力:校验、示例、别名等都可以原样使用。

在入口函数层面,fastapi/param_functions.py 定义了 Form(...) 便捷函数,其返回的正是 params.Form(...) 实例。它暴露的完整参数如下:

参数 默认值 作用
default Undefined 字段缺省时的默认值;不传即为必填
default_factory 未设置 生成默认值的可调用对象
media_type "application/x-www-form-urlencoded" 声明字段的媒体类型,会影响生成的 OpenAPI(注:按当前源码注释,尚不影响实际数据解析方式)
alias / alias_priority / validation_alias / serialization_alias None / 自动 给字段起别名,例如把 Python 关键字或含 - 的名称映射为真实表单字段名(如 user-name 代替 username
title / description None 字段标题与描述,进入 OpenAPI 文档
gt / ge / lt / le None 数值大小校验(大于/大于等于/小于/小于等于)
multiple_of None 数值须为该值的倍数
allow_inf_nan 未设置 数值是否允许 inf/-inf/nan
max_digits / decimal_places 未设置 十进制数值的最大位数 / 最大小数位
min_length / max_length None 字符串最小/最大长度
pattern None 字符串正则校验;旧参数 regex 已在 FastAPI 0.100.0 起弃用,应改用 pattern
strict 未设置 是否启用严格类型校验
examples / example / openapi_examples None 示例数据(example 在 OpenAPI 3.1 下已弃用,优先用 examples/openapi_examples
deprecated None 标记字段为废弃
include_in_schema True 是否把该字段纳入生成的 OpenAPI
json_schema_extra None 附加的 JSON Schema 数据

因此表单字段同样可以获得与 Body 一致的校验与文档能力,例如:

@app.post("/items/")
async def create_item(
    name: Annotated[str, Form(min_length=3, max_length=50)],
    count: Annotated[int, Form(ge=0)],
):
    return {"name": name, "count": count}

表单编码的技术细节:urlencoded 与 multipart

FastAPI 官方教程的"Technical Details"一节强调了两点:

  1. 普通表单数据的媒体类型是 application/x-www-form-urlencoded
  2. 当表单包含文件时,编码切换为 multipart/form-data

这也就是为什么纯文本字段用 Form、而文件上传用 File(后者把 media_type 预设为 multipart/form-data,见 fastapi/param_functions.pyFile 的默认值)。若想系统学习表单字段与这两种编码的 HTTP 语义,可进一步查阅 MDN Web Docs 中关于 POST 方法及表单编码的说明章节。

源码视角:FastAPI 是如何解析表单的

从请求入口到参数注入,表单请求经历了如下关键链路:

  1. 路由处理阶段先判断是否存在表单 body。在 fastapi/routing.py 中,is_body_form = body_field and isinstance(body_field.field_info, params.Form)——只要 body 字段是 Form 实例,就走 body = await request.form() 读取 FormData;否则才按 JSON/字节读取 request.body()。这就是"FastAPI 从正确的位置读取数据"的落点。
  2. 校验阶段,fastapi/dependencies/utils.pyrequest_body_to_args 会识别 received_body 是否为 FormData:若是,则先由 _extract_form_body 按字段别名/名称从多值字典(Multidict)中取回各字段值,再逐个走 Pydantic 校验,最终得到干净的 values 注入端点函数。

也就是说:声明了 Form,解析、取值与校验路径都和 JSON 完全分离,行为是确定且可预期的。

关键警告:Form 与 JSON Body 不能混用

教程给出了一条必须牢记的规则:可以在一个 path operation 里声明多个 Form 参数,但不能同时声明期望以 JSON 接收的 Body 字段。原因在于 HTTP 请求体在某一时刻只能有一种编码:一旦声明了表单字段,请求体的媒体类型就是 application/x-www-form-urlencoded(有文件时是 multipart/form-data),不再可能是 application/json

官方文档特别澄清:这不是 FastAPI 的限制,而是 HTTP 协议本身的约束。如果确实需要同时发送表单字段与 JSON,业界常见的替代方案是:要么全部用 JSON(前端自行拼装),要么把结构化数据作为字段值发送并在服务端再解析,或者改用 multipart 逐个字段承载。

通过仓库测试验证行为:成功、缺字段与发错格式

教程配套的测试文件 tests/test_tutorial/test_request_forms/test_tutorial001.py 用参数化 fixture 同时驱动 tutorial001_py310tutorial001_an_py310 两个示例模块,逐条验证了本文上述结论:

  • 正常提交client.post("/login/", data={"username": "Foo", "password": "secret"}) 返回 200{"username": "Foo"}——注意这里用的是 data=(表单编码)而不是 json=
  • 缺失任一字段:只发 username 或只发 password 均返回 422,错误项 loc["body", "password"]/["body", "username"]typemissing
  • 空请求:同样返回 422,且 usernamepassword 两个缺失错误同时出现;
  • 误发 JSON:用 json={"username": "Foo", "password": "secret"} 提交,仍返回 422 missing——因为该端点被识别为表单端点,会按表单路径解析(得到的表单为空),JSON 载荷中的字段不会被"看见"。这说明面向表单端点时,客户端必须以 application/x-www-form-urlencoded 编码发送字段;
  • OpenAPI 一致性/openapi.json/login/requestBody.content 键为 application/x-www-form-urlencoded,其 schema 为 Body_login_login__postrequired["username", "password"],两个属性类型均为 string。这直观印证了 Form 声明如何被翻译成表单类型的 OpenAPI 请求体定义。

除教程测试外,仓库在 tests/test_request_params/test_form/ 下还维护着一套更细粒度的表单参数测试(必填、可选、list 类型等),并在 tests/test_multipart_installation.py 覆盖了未安装 python-multipart 时的报错路径,可作为深入理解表单解析行为的参考。

小结

  • 接收表单字段而不是 JSON 时,导入并使用 Form
  • 使用前先安装 python-multipartuv add python-multipartpip install python-multipart);
  • Form 直接继承自 Body,可复用校验、别名、示例等全部配置;
  • 普通表单对应 application/x-www-form-urlencoded,含文件的表单对应 multipart/form-data
  • 声明了 Form 的端点不能再声明 JSON 形式的 Body 字段,这是 HTTP 协议层面的约束;
  • OAuth2 密码流要求 username/password 以表单字段发送,Form 是声明这类登录/令牌端点的基础设施。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388