在 Web 开发中,表单(Form)是最传统也是使用最广泛的客户端提交数据方式。无论是登录注册、搜索筛选,还是传统的 HTML 页面提交,表单数据都无处不在。FastAPI 通过 Form 提供了对 application/x-www-form-urlencodedmultipart/form-data 两种表单编码的原生支持。本文带你系统掌握 FastAPI 中的表单处理技巧。

基础表单字段

FastAPI 使用 Form 来声明表单字段,用法与 QueryPath 非常相似:

1
2
3
4
5
6
7
8
9
10
11
from fastapi import FastAPI, Form

app = FastAPI()


@app.post("/login/")
async def login(
username: str = Form(..., description="用户名"),
password: str = Form(..., description="密码"),
):
return {"username": username, "status": "ok"}

几个关键点:

  • Form(...) 中的 ...(Ellipsis)表示该字段是必填的,等同于 Form() 不带默认值。
  • 如果给默认值(如 Form("guest")),则字段变为可选。
  • 表单字段支持 min_lengthmax_lengthregex 等校验参数,和 QueryPath 完全一致。
  • 使用 Form 时需要在路由装饰器中不要指定 response_model 为 Pydantic 模型时混合使用 body 参数,否则会冲突(下文详解)。

可选字段与默认值

对于可选字段,提供默认值即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
@app.post("/search/")
async def search(
keyword: str = Form("", description="搜索关键词"),
page: int = Form(1, ge=1, description="页码"),
page_size: int = Form(20, ge=1, le=100, description="每页数量"),
sort: str | None = Form(None, description="排序字段"),
):
return {
"keyword": keyword,
"page": page,
"page_size": page_size,
"sort": sort,
}

这里用 str | None = Form(None) 表示该字段可以为空——客户端不传该字段时,值为 None

多选值与列表字段

HTML 表单中常有 select multiple 或同名的 checkbox 组,传入的是一个值列表。FastAPI 通过 typing.List 来接收:

1
2
3
4
5
6
7
8
9
from typing import List


@app.post("/subscribe/")
async def subscribe(
email: str = Form(...),
topics: List[str] = Form([], description="订阅主题"),
):
return {"email": email, "topics": topics}

客户端发送 topics=python&topics=fastapi&topics=ai 时,服务端收到的 topics 就是 ["python", "fastapi", "ai"]

文件与表单混合上传

最常见的高级场景是同时上传文件和其他表单字段(如用户头像 + 昵称)。此时需要用到 FileForm 的组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from fastapi import FastAPI, File, Form, UploadFile

app = FastAPI()


@app.post("/upload-avatar/")
async def upload_avatar(
nickname: str = Form(..., description="用户昵称"),
avatar: UploadFile = File(..., description="头像文件"),
):
file_content = await avatar.read()
file_size = len(file_content)
return {
"nickname": nickname,
"filename": avatar.filename,
"size": file_size,
}

注意: 当路由中同时存在 FormFile 参数时,请求的 Content-Type 自动变为 multipart/form-data。如果只有 Form 参数,默认使用 application/x-www-form-urlencoded

表单数据与 Pydantic 模型

你可能会想把 Form 字段用 Pydantic 模型封装起来。但需要注意的是,表单数据和 JSON 请求体的处理路径不同——Pydantic 默认从 JSON body 读取数据,而 Form 从表单字段读取,二者不可直接互换。

推荐的模式是用一个独立的依赖函数来组织表单字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from fastapi import Depends


class RegistrationForm:
def __init__(
self,
username: str = Form(..., min_length=3, max_length=50),
email: str = Form(..., regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"),
password: str = Form(..., min_length=8),
confirm_password: str = Form(..., min_length=8),
):
self.username = username
self.email = email
self.password = password
self.confirm_password = confirm_password


@app.post("/register/")
async def register(form: RegistrationForm = Depends()):
if form.password != form.confirm_password:
return {"error": "两次密码不一致"}
# 后续注册逻辑...
return {"username": form.username, "email": form.email, "status": "registered"}

这样表单字段的定义被封装在 RegistrationForm 类中,路由函数通过 Depends() 获取实例,代码清晰且可复用。

手动处理表单的原始请求

在某些特殊场景下(如需要访问表单的键值对而不预先定义结构),可以直接从 Request 对象获取原始表单数据:

1
2
3
4
5
6
7
8
9
10
from fastapi import Request


@app.post("/raw-form/")
async def raw_form(request: Request):
form_data = await request.form()
result = {}
for key, value in form_data.items():
result[key] = value
return {"fields": result, "count": len(result)}

request.form() 返回一个 FormData 对象(类似字典),可以迭代所有字段,每个值都是 FormDatastrUploadFile 类型。

表单编码的类型选择

FastAPI 根据路由参数自动推断合适的 Content-Type:

路由参数类型 自动 Content-Type
仅有 Form application/x-www-form-urlencoded
File / UploadFile multipart/form-data
Form + File 混合 multipart/form-data

对于简单的文本字段(如登录表单),x-www-form-urlencoded 足够且开销更小;涉及文件上传或大量数据时,multipart/form-data 是唯一选择。

小结

FastAPI 的 Form 让表单数据处理和查询参数一样直观——声明式字段定义、内建校验、自动文档生成。总结几个要点:

  1. Form(...) 声明必填字段,Form(default) 声明可选字段。
  2. List[str] 类型注解接收同名多值字段。
  3. Form + File 混合时走 multipart/form-data,仅 Form 时走 x-www-form-urlencoded
  4. 表单字段可用 Depends + 类封装实现结构化组织。
  5. 可通过 request.form() 获取原始表单数据做自由解析。

掌握这些,你就能从容应对各种表单提交场景了。