首页
/ FastAPI 表单模型(Form Models)实战:用 Pydantic 模型统一声明与校验表单字段

FastAPI 表单模型(Form Models)实战:用 Pydantic 模型统一声明与校验表单字段

2026-09-07 16:04:28作者:管翌锬

自 FastAPI 0.113.0 起,你可以直接用一个 Pydantic 模型 来描述整个 HTML 表单,FastAPI 会把请求中的每个表单字段分别抽取出来、完成类型校验,再以你定义好的模型实例交给路径操作函数。本指南以 官方表单模型教程 为主线,结合本仓库中对应的 示例源码测试用例,讲解 Pydantic 模型表单的声明方式、交互文档验证、以及如何用 extra = "forbid" 拒绝额外字段,读完即可直接落地到登录、注册、搜索筛选等典型场景。

适用场景:什么时候该用"表单模型"

在引入 Form Models 之前,声明多个表单字段的常规做法是逐个写出 Form 参数。而"表单模型"的价值在于:

  • 把一组平铺的表单字段收敛为一个类型usernamepassword 等字段在服务端统一归入同一个 Pydantic 模型,逻辑上内聚,语义上等价于"提交一份 FormData"。
  • 复用 Pydantic 的能力:字段类型、必填约束、默认值、校验规则都可以集中定义在该模型中。
  • 自动生成 OpenAPI:交互文档(/docs)会明确展示请求体为 application/x-www-form-urlencoded,并引用该模型的 Schema。

需要特别注意的是:表单编码(application/x-www-form-urlencodedmultipart/form-data)传输的是扁平的 key=value 键值对,因此这里的模型字段适合声明为字符串、数字、布尔值等基础标量类型;如果你要接收的是 JSON 结构体,应使用 Body / Pydantic 模型配合 JSON 请求体,而不是 Form

前置条件:安装 python-multipart 与版本要求

FastAPI 解析表单数据依赖第三方库 python-multipart(注意:python-multipart 的解析逻辑在请求进入路由层时由 FastAPI 内部调用,你无需直接 import 它)。在原文档中明确要求先将其加入项目依赖,推荐使用项目当前使用的 uv 包管理器:

$ uv add python-multipart

使用 pip 时等价于:

$ pip install python-multipart

同时请留意版本前提:

  • 使用 Pydantic 模型声明表单字段 需要 FastAPI 0.113.0 及以上版本(原文档注明的支持起始版本);
  • 使用下文"禁止额外字段"能力需要 FastAPI 0.114.0 及以上版本。

本仓库中该教程的示例源码均以 Python 3.10+ 语法编写(文件名含 _py310 后缀,并使用 X | NoneAnnotated 等新语法),运行环境应满足这一前提。

核心用法:声明一个 Pydantic 表单模型

只需要两步:

  1. pydantic.BaseModel 定义一个模型,其中的每个字段都对应表单中的一个字段;
  2. 在路径操作函数中,把参数类型标注为这个模型,并用 Form() 显式声明它来自表单数据。

官方推荐使用 Annotated 风格的写法(见 tutorial001_an_py310.py):

from typing import Annotated

from fastapi import FastAPI, Form
from pydantic import BaseModel

app = FastAPI()


class FormData(BaseModel):
    username: str
    password: str


@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
    return data

如果你所在的代码库仍使用"默认值"风格声明参数,也可以等价写作(见 tutorial001_py310.py):

from fastapi import FastAPI, Form
from pydantic import BaseModel

app = FastAPI()


class FormData(BaseModel):
    username: str
    password: str


@app.post("/login/")
async def login(data: FormData = Form()):
    return data

当客户端以表单方式 POST /login/,例如发送 username=Foo&password=secretapplication/x-www-form-urlencoded),FastAPI 会:

  1. 解析请求体中的表单数据;
  2. 依据 FormData 模型,把每个字段的类型校验、必填校验交给 Pydantic 执行;
  3. 若校验通过,构造并返回 FormData 实例给 data 参数。

示例中的函数直接把模型实例作为 JSON 返回,因此客户端会收到形如 {"username": "Foo", "password": "secret"} 的响应。

用交互文档验证:/docs 与 OpenAPI Schema

启动应用后打开 http://127.0.0.1:8000/docs,找到 POST /login/ 接口即可验证:交互文档会展示"请求体字段"(Request body)为 application/x-www-form-urlencoded,并列出 username(必填字符串)与 password(必填字符串)两个字段,与表单模型的声明一一对应。

FastAPI /docs 交互文档中 /login 表单模型请求体的展示

这一表现并非只停留在 UI 层,它源自自动生成的 OpenAPI Schema。仓库中的测试 test_tutorial001.py 直接断言了 /openapi.json 的内容结构,其中关键部分如下:

  • 请求体媒体类型被声明为 application/x-www-form-urlencoded,其 schema 通过 $ref 指向 #/components/schemas/FormData
  • components.schemas.FormDataproperties 定义了 usernamepassword 两个字符串字段,required 数组为 ["username", "password"]
  • 接口同时自动携带 422 校验错误响应。

也就是说,模型字段的必填性、类型都同步映射进了 OpenAPI,前端可以直接据此生成类型安全的客户端代码。

缺失字段与错误请求的行为:来自测试用例的证据

为了把"表单模型"的校验语义讲透,仓库测试 test_tutorial001.py 针对同一接口覆盖了多种请求,可以直接当作行为契约阅读:

  • 提交完整字段data={"username": "Foo", "password": "secret"} 返回 200,响应体为 {"username": "Foo", "password": "secret"}
  • 只提交 username:返回 422,错误 detailmissingloc 指向 ["body", "password"]
  • 只提交 password:返回 422loc 指向 ["body", "username"]
  • 完全不提交数据:返回 422,且同时报告 usernamepassword 两个字段缺失;
  • 错误地提交 JSON 请求体json={"username": "Foo", ...}):同样返回 422——因为该路径操作声明的请求体是表单而非 JSON,解析出的表单数据为空,因而按字段缺失处理。

这组测试清楚说明了表单模型的校验本质:缺失字段不是靠手写判断,而是由 Pydantic 模型的必填约束驱动,校验错误统一走 FastAPI 的 422 校验错误响应格式(type / loc / msg / input)。

进阶:用 extra = "forbid" 拒绝额外表单字段

默认情况下,Pydantic 模型对未声明的额外字段采取忽略策略。也就是说客户端即使多提交一个模型里不存在的字段,接口也能正常返回 200。在一些对入参有严格白名单要求的场景(原文档也指出这类需求"可能并不常见"),你可能希望只允许模型声明过的字段、其余一律拒绝。

从 FastAPI 0.114.0 开始支持在表单模型上启用这一限制。做法是在模型的 model_config 中设置 extra"forbid",示例见 tutorial002_an_py310.py

from typing import Annotated

from fastapi import FastAPI, Form
from pydantic import BaseModel

app = FastAPI()


class FormData(BaseModel):
    username: str
    password: str
    model_config = {"extra": "forbid"}


@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
    return data

等价配置也可以写作 model_config = ConfigDict(extra="forbid"),效果相同,均可作用于表单校验。

启用后,如果客户端尝试提交这些字段:

  • usernameRick
  • passwordPortal Gun
  • extraMr. Poopybutthole

接口将返回 422 错误响应,明确告知 extra 字段不被允许:

{
    "detail": [
        {
            "type": "extra_forbidden",
            "loc": ["body", "extra"],
            "msg": "Extra inputs are not permitted",
            "input": "Mr. Poopybutthole"
        }
    ]
}

错误语义要点:

  • typeextra_forbidden,这是 Pydantic v2 对额外输入的专用错误类型;
  • loc["body", "extra"],说明错误发生在"请求体"层级的 extra 字段上(与 JSON 请求体中拒绝多余字段的错误定位方式一致);
  • input 回显了被拒绝的原始值,便于调试。

对应行为在测试 test_tutorial002.py 中被完整断言:提交带 extra 字段的表单返回 422 与上述结构一致的错误体。同时该测试文件还验证了即使开启 extra = "forbid",正常字段(含全量提交、单字段缺失、空提交等场景)的校验行为与未开启时保持一致,且 OpenAPI 中 FormData 的 schema 会额外带有 "additionalProperties": false,把"禁止额外字段"这一约束同步暴露给文档与客户端代码生成器。

源码视角:Form() 与表单模型是如何工作的

从底层实现看,Form() 本质上是构造一个携带表单参数信息的对象。在 fastapi/param_functions.py 中,Form 函数的签名体现了表单字段的默认行为:

  • default:字段缺省时的默认值;
  • media_type:默认 "application/x-www-form-urlencoded",会影响生成的 OpenAPI 文档(在接收文件时配合 File / multipart 使用);
  • alias / alias_priority 等:用于给字段设置别名及别名生成优先级,与 Pydantic 字段的别名机制保持一致。

当参数类型是一个 Pydantic BaseModel、同时以 Form() 标注时,FastAPI 会按"请求体"来处理该参数——这解释了为什么上面的校验错误 loc 都指向 ["body", ...],也解释了为什么测试中通过 client.post("/login/", data={...}) 发送表单即可命中对应模型字段、而发送 JSON 会被当作空表单处理。

结合 test_tutorial001.py 的写法可以看到:两个风格变体(tutorial001_py310tutorial001_an_py310)通过同一组 pytest 参数化用例验证,说明无论用 Annotated[FormData, Form()] 还是 FormData = Form(),最终校验语义完全一致。若你想在本地复现,运行:

$ pytest tests/test_tutorial/test_request_form_models/

即可同时执行两套教程示例的全部行为测试与 OpenAPI Schema 快照断言。

小结

FastAPI 的 Form Models 把"多个表单字段 + Pydantic 校验 + OpenAPI 文档"统一到一个模型声明中,是处理登录、筛选器等表单类接口的简洁方案。本仓库中可继续深入阅读的素材包括:

要点回顾:安装 python-multipart(FastAPI 0.113.0+);用 BaseModel 定义字段并通过 Form() 声明;默认忽略额外字段,需要严格入参时用 model_config = {"extra": "forbid"}(FastAPI 0.114.0+),届时多余字段会以 extra_forbidden422 错误被拒绝。

登录后查看全文
热门项目推荐
相关项目推荐