首页
/ FastAPI 同时接收文件与表单数据:File 与 Form 参数混合使用实战指南

FastAPI 同时接收文件与表单数据:File 与 Form 参数混合使用实战指南

2026-09-06 12:59:54作者:温玫谨Lighthearted

在构建上传类 API 时,一个常见需求是:同一次 HTTP 请求既要携带文件(图片、附件、音视频),又要携带若干表单字段(如 token、分类、备注等)。FastAPI 通过 FileForm 两个参数声明器支持这种混合场景:把文件参数和表单字段统一声明在同一个路径操作函数中,FastAPI 会将整个请求体按 multipart/form-data 编码解析。读完本篇,你将掌握如何在 FastAPI 中同时声明 FileForm 参数、理解 bytesUploadFile 两种文件类型的差异、了解 OpenAPI 对该混合请求体的建模方式,以及从源码层面理解 FastAPI 对 multipart 依赖的检查机制。

前置条件:安装 python-multipart

要接收上传的文件和/或表单数据,必须先安装 python-multipart 包(它负责解析 multipart/form-data 请求体)。将其加入项目:

$ uv add python-multipart

如果你使用 pip,等价命令为 pip install python-multipart

这一点在源码中得到了印证。FastAPI 在 fastapi/dependencies/utils.py 中定义了 ensure_multipart_is_installed(),一旦检测到使用了 Form/File 参数却没有正确安装依赖,就会抛出带明确指引的 RuntimeError

  • 未安装时提示 Form data requires "python-multipart" to be installed
  • 特别地,如果你误装成了名字相近的 multipart 包,错误信息会额外提示先执行 pip uninstall multipart 再安装正确的 python-multipart。这个检查通过尝试导入 python_multipart.__version__parse_options_header 来区分两者。

该检查的触发点在参数解析阶段:fastapi/dependencies/utils.py 中,只要某个参数的 field_infoForm 实例(FileForm 的子类,因此同样命中),就会调用 ensure_multipart_is_installed()。也就是说,缺失依赖会在应用构建路由时报错,而不是等到第一次请求才失败。

导入 File 和 Form

示例代码来自仓库中的 docs_src/request_forms_and_files/tutorial001_an_py310.py。首先从 fastapi 中导入 FileForm 以及 UploadFile

from fastapi import FastAPI, File, Form, UploadFile

fastapi/params.py 中可以确认三者的类继承关系:Form 继承自 Bodyfastapi/params.py),而 File 又继承自 Formfastapi/params.py)。这个继承链决定了两件事:

  1. 参数能力一致FileForm 都能接收 aliastitledescriptionmin_lengthpattern 等与 Body/Query 相同的一套校验参数,声明方式和声明 JSON Body 或查询参数时完全一致;
  2. 媒体类型不同Form 的默认 media_typeapplication/x-www-form-urlencoded,而 File 将默认 media_type 覆盖为 multipart/form-data。当操作函数中出现了 File 参数,FastAPI 会把整个请求体视为 multipart/form-data,表单字段也随之作为 multipart 表单字段传输。

定义 File 与 Form 参数

下面是一个完整的可运行示例(docs_src/request_forms_and_files/tutorial001_an_py310.py):

from typing import Annotated

from fastapi import FastAPI, File, Form, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(
    file: Annotated[bytes, File()],
    fileb: Annotated[UploadFile, File()],
    token: Annotated[str, Form()],
):
    return {
        "file_size": len(file),
        "token": token,
        "fileb_content_type": fileb.content_type,
    }

如果不使用 Annotated,等价的旧式写法见 docs_src/request_forms_and_files/tutorial001_py310.py

@app.post("/files/")
async def create_file(file: bytes = File(), fileb: UploadFile = File(), token: str = Form()):
    ...

两个要点

文件与表单字段以表单数据形式一起上传。 客户端发送的请求体是 multipart/form-data,其中 filefileb 是文件部分(part),token 是普通表单字段部分,服务端在同一个函数签名里分别接收它们。

文件参数可以选择 bytesUploadFile 两种类型。 上例刻意演示了两种并存:

  • file: Annotated[bytes, File()]:文件内容被一次性读入内存并作为 bytes 传给函数,用 len(file) 即可得到字节大小;
  • fileb: Annotated[UploadFile, File()]:得到 UploadFile 对象,可异步分块读写,并携带 filenamesizecontent_typeheaders 等元数据。

UploadFilefastapi/datastructures.py 中继承自 Starlette 的 UploadFile,属性定义非常清晰:file(标准 Python 文件对象,同步访问用)、filename(原始文件名)、size(字节大小)、headers(该 part 的请求头)、content_type(如 text/plainimage/png)。它的 read/write/seek/close 方法都是 async 的,底层通过线程池执行,因此适合在 async def 路径操作中分块读取大文件,避免阻塞事件循环。

警告:不能与 JSON Body 字段混用

你可以声明多个 FileForm 参数,但不能同时声明期望以 JSON 接收的 Body 字段。因为此时请求体被 multipart/form-data 编码,而不是 application/json。这不是 FastAPI 的限制,而是 HTTP 协议本身的约束:一个请求体的 Content-Type 只能是其中一种。

从源码结构看,这个约束体现在 OpenAPI 建模阶段:测试快照显示该示例生成的 OpenAPI 请求体只有 multipart/form-data 一种 content 类型(见下节),FastAPI 不会为一个操作同时生成 JSON 与 multipart 两种请求体定义。如果你确实需要"文件 + 结构化 JSON 数据",可行做法是把结构化数据编码为表单字段,或使用 Form 接收模型/字典后自行解析,具体可参考 docs/en/docs/tutorial/request-form-models.md

请求与响应行为(结合测试验证)

仓库中的测试文件 tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py 对上面的示例做了完整的行为验证,值得直接借鉴为调用方式参考:

成功请求:用 TestClient(或任何 HTTP 客户端)同时发送表单数据和两个文件:

response = client.post(
    "/files/",
    data={"token": "foo"},
    files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")},
)
assert response.status_code == 200
assert response.json() == {
    "file_size": 14,
    "token": "foo",
    "fileb_content_type": "text/plain",
}

可以看到:bytes 文件按字节长度返回 14,表单字段 token 原样返回,UploadFilecontent_type 准确保留了客户端声明的 text/plain

缺失字段的 422 校验:测试还验证了三种失败场景,均返回 422,loc 统一指向 body 下的具体字段名(filefilebtoken):

  • 不发送任何数据时,三个字段全部 missing
  • 只发送 data={"token": "foo"} 不带文件时,两个文件字段 missing
  • json={"file": "Foo", "token": "Bar"} 以 JSON 发送时同样失败——再次印证了 JSON Body 与 Form/File 不兼容的结论。

OpenAPI 建模:测试中对 /openapi.json 的快照断言展示了混合参数的 schema 形态:

{
  "requestBody": {
    "content": {
      "multipart/form-data": {
        "schema": {"$ref": "#/components/schemas/Body_create_file_files__post"}
      }
    },
    "required": true
  }
}

对应的 Body_create_file_files__post 组件中,filefileb 被建模为带 "contentMediaType": "application/octet-stream"string 类型,token 则是普通 string,三者都在 required 列表中。UploadFile 之所以映射成这种 schema,来自 fastapi/datastructures.py__get_pydantic_json_schema__ 的固定返回:{"type": "string", "contentMediaType": "application/octet-stream"}。这使得 Swagger UI 中的上传控件能正确渲染文件选择器。

小结

  • 需要在同一个请求中同时接收数据和文件时,把文件参数声明为 File()、表单字段声明为 Form(),二者共享同一套参数校验能力(继承自 Body,见 fastapi/params.py);
  • 文件参数可按需选择 bytes(整体进内存、适合小文件)或 UploadFile(异步分块读写、携带元数据,适合大文件);
  • 必须先安装 python-multipart,且 FastAPI 会在解析到 Form/File 参数时主动检查依赖安装(fastapi/dependencies/utils.py);
  • File/Form 参数所在的请求体以 multipart/form-data 编码,不能与 JSON Body 字段混用,这是 HTTP 协议层面的约束;
  • 生成的 OpenAPI 会将整个请求体建模为单一的 multipart/form-data 请求体,文件字段带 contentMediaType: application/octet-stream,便于文档与客户端生成工具正确理解接口。
登录后查看全文
热门项目推荐
相关项目推荐