首页
/ FastAPI 表单模型(Form Models)实战指南:用 Pydantic 模型声明表单字段

FastAPI 表单模型(Form Models)实战指南:用 Pydantic 模型声明表单字段

2026-09-06 18:44:12作者:庞队千Virginia

本文基于当前仓库中的 request-form-models 教程 展开,系统讲解如何在 FastAPI 路径操作中直接用一个 Pydantic 模型 声明一组表单字段(form fields):从依赖安装、声明语法、底层解析原理,到如何通过模型配置禁止多余字段,并给出源码与测试层面的验证依据。读完本文你将掌握把「登录表单、注册表单」这类多个字段整体收进一个模型、由 FastAPI 自动抽取并校验的实战写法。

为什么需要 Form Models

传统的「单个字段逐个声明」写法依赖大量重复代码:每声明一个表单字段都要写一次参数注解、别名、默认值;而 FastAPI 的 Form Models 允许你把多个表单字段集中定义在一个 Pydantic BaseModel 中,然后在路径操作函数里用 Form() 标注这一模型参数,FastAPI 会自动把请求体(application/x-www-form-urlencoded 表单数据)中每个键的值抽取出来,填入模型实例后再交给你。

要使用这一能力(包括普通 Form 单参数形式),必须先安装 python-multipart,否则 FastAPI 会在运行期抛出缺少依赖的异常。官方文档给出了 uv 安装方式:

$ uv add python-multipart

如果使用其他包管理器,等价做法是安装名为 python-multipart 的第三方解析包,它是 FastAPI 解析 multipart/form-data 与 URL 编码表单数据的底层依赖。

两个版本说明值得留意:

  • Form Models(用 Pydantic 模型声明表单字段)从 FastAPI 0.113.0 版本开始支持;
  • 「禁止多余表单字段」的模型配置特性从 0.114.0 版本开始支持。

用 Pydantic 模型声明表单字段

完整示例代码

先看最基础的用法(本仓库的示例源码位于 docs_src/request_form_models/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

这里的关键步骤有两处:

  1. 定义一个 Pydantic 模型 FormData,把需要接收的字段 usernamepassword 都声明为 str
  2. 在路径操作参数中使用 Annotated[FormData, Form()],把该模型参数标记为表单数据。

当客户端以 POST /login/ 发送 Content-Type: application/x-www-form-urlencoded(或 multipart/form-data)请求、携带 usernamepassword 两个键时,FastAPI 会为模型中的每个字段从表单数据中抽取对应值,再组装成 FormData 实例传入函数体。返回时 data 就是一个真实的 FormData 对象。

两种等价的标注语法

当前仓库的 docs_src/request_form_models 目录同时提供两种写法的示例:

  • tutorial001_an_py310.py:基于 typing.Annotated 的推荐写法,即上面展示的 data: Annotated[FormData, Form()]
  • tutorial001_py310.py:不使用 Annotated 的传统写法:
@app.post("/login/")
async def login(data: FormData = Form()):
    return data

两种写法功能等价。Annotated 写法的优势在于元数据与默认值分离,便于在保留默认值语义的同时显式声明校验/类型信息,也是当前 FastAPI 文档主推的风格。

底层是如何把模型拆成表单字段的

从源码可以看清这套机制的实际调用链。表单数据与 JSON 请求体走的是完全不同的解析路径:

if (
    single_not_embedded_field
    and lenient_issubclass(first_field.field_info.annotation, BaseModel)
    and isinstance(received_body, FormData)
):
    fields_to_extract = get_cached_model_fields(first_field.field_info.annotation)
  • 随后 _extract_form_body()(见 fastapi/dependencies/utils.py#L912)逐个字段从 FormData(Starlette 的多值字典)中取值——若注解是序列类型则通过 getlist() 取多值,普通字段则取单值,再按字段校验别名写入字典;最后把整份字典交给模型做整体校验。

  • 当任一参数被标记为 Form 时,FastAPI 会在解析阶段调用 ensure_multipart_is_installed()(见 fastapi/dependencies/utils.py#L522-L523)确认 python-multipart 已安装,否则抛出明确提示;同时路由层通过 isinstance(body_field.field_info, params.Form) 判定该操作需要按表单而非 JSON 处理(见 fastapi/routing.py#L395)。

  • Form 参数类本身继承自 Body,其默认 media_typeapplication/x-www-form-urlencoded(见 fastapi/params.py#L581-L588),因此生成的 OpenAPI 请求体媒体类型是表单而非 JSON。

换句话说:一个 Pydantic 模型注解 + Form(),会被 FastAPI 在内部展开为若干个表单字段分别抽取,再合并回模型做统一校验,对调用方而言体验与「直接传一个模型对象」完全一致。

在 /docs 文档界面中查看效果

启动应用后访问 /docs,FastAPI 的交互式文档(Swagger UI)会自动把该接口渲染成表单填写界面,效果如下:

FastAPI /docs 中 POST /login/ 表单接口的交互式文档界面,展示由 FormData 模型生成的 username 与 password 必填表单字段

从上图可以看到几个由 Form Models 自动生成的元素:

  • 请求体媒体类型为 application/x-www-form-urlencoded,对应 Form() 的默认 media_type
  • usernamepassword 两个字段以独立的表单输入框呈现,均标为 * required(对应模型中的必填 str 字段);
  • 开发者无需手写任何表单 HTML,直接点击「Try it out」填入值即可发起请求验证。

禁止多余的表单字段(extra = "forbid")

在大多数场景下,FastAPI 会忽略请求中出现在模型之外的额外表单字段。但某些特殊业务里你可能希望严格限制:客户端只能发送模型中声明过的字段,任何多余字段都直接报错。这可以通过 Pydantic 的 model_config 配置实现。

模型配置写法

在模型类中设置 model_config = {"extra": "forbid"} 即可(示例见 docs_src/request_form_models/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

"extra": "forbid" 是 Pydantic v2 的模型行为配置:解析时一旦发现输入中存在模型未声明(且在 model_fields 之外的键),即产生校验错误。传统写法同样成立,见 docs_src/request_form_models/tutorial002_py310.py

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

多余字段时的报错响应

假设客户端尝试提交下面三个表单字段:

  • username: Rick
  • password: Portal Gun
  • extra: Mr. Poopybutthole

由于 extra 不在 FormData 中且配置了 extra = "forbid",客户端将收到 422 Unprocessable Entity 错误,响应体如下:

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

其中:

  • type: "extra_forbidden" 表示触发了 Pydantic v2 的「额外输入被禁止」校验规则;
  • loc: ["body", "extra"] 指向出错位置——位于 body(表单)中的 extra 键;
  • input 回显了引发错误的原始值,便于客户端定位问题。

测试用例的验证

仓库中的测试对上述行为做了完整覆盖。以 tests/test_tutorial/test_request_form_models/test_tutorial002.py 为例:

  • test_post_body_form:提交合法字段 usernamepassword,断言返回 200 且响应体与表单数据一致;
  • test_post_body_extra_form:额外提交 extra 字段,断言返回 422,且错误 JSON 中 typeextra_forbiddenloc["body", "extra"]
  • test_post_body_form_no_password / test_post_body_form_no_username / test_post_body_form_no_data:分别验证缺少必填字段时返回 422,错误类型为 missingloc 指向缺失字段,input 为当前已收到的其余数据;
  • test_post_body_json:用 JSON 而非表单格式提交同样会被拒绝(返回 422),印证该接口只接受表单媒体类型;
  • test_openapi_schema:通过快照断言 OpenAPI schema 中生成的表单字段与配置一致。

对应地,tests/test_tutorial/test_request_form_models/test_tutorial001.py 覆盖了未开启 forbid 时的基础场景。测试夹具同时对 tutorial00x_py310tutorial00x_an_py310 两个版本运行(见测试文件顶部的 @pytest.fixture 参数化),保证两种标注语法行为一致。

与其他表单特性的衔接

Form Models 是 request-forms 教程(以 Form 接收单个表单参数)的进阶组合:当表单字段数量增多、字段间存在整体校验或复用需求时,把字段收拢到 Pydantic 模型是更清晰、更易测试的组织方式。二者使用同一套底层表单解析逻辑与依赖要求(python-multipart),因此可以按需混用。

使用 Form Models 时需注意:

  • 该能力要求 FastAPI 0.113.0+extra = "forbid" 行为要求 0.114.0+,请确认运行环境的版本满足要求;
  • 由于字段最终以表单键值对传输,字段名应保持合法的表单键名,并可结合 Pydantic 的字段别名能力做命名映射;
  • 若要接收文件上传(multipart 场景),可继续结合 FileUploadFile 使用,但表单模型与文件字段的声明方式需要分开设计。

小结

在 FastAPI 中,你只需定义一个 Pydantic 模型、用 Form() 标注参数,即可把散落的多个表单字段一键收进模型统一接收与校验;需要严格模式时,通过 model_config = {"extra": "forbid"} 拒绝多余字段,客户端超发数据会收到包含 extra_forbidden 详情的 422 响应。这套机制从声明、抽取、整体校验到自动生成 /docs 交互式表单界面,全程由 FastAPI 的依赖注入与 Pydantic 校验管线串联完成,源码级佐证可分别回溯到 fastapi/dependencies/utils.pyfastapi/params.pyfastapi/routing.py,并已被 tests/test_tutorial/test_request_form_models 下的测试完整覆盖。

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