首页
/ FastAPI 表单与文件混合上传:在同一个 multipart/form-data 请求中声明 File 与 Form 参数

FastAPI 表单与文件混合上传:在同一个 multipart/form-data 请求中声明 File 与 Form 参数

2026-09-06 18:44:48作者:齐冠琰

在 Web 开发中,经常遇到「上传文件 + 附带几个普通字段」的需求,例如上传头像时同时携带 token 做鉴权,或提交文件的同时附带描述性元数据。本文基于 FastAPI 官方教程 Request Forms and Files 展开讲解:如何在同一个路径操作中同时使用 FileForm 声明文件与表单字段参数,让客户端一次性以 multipart/form-data 编码把二者提交上来。读完本文你将掌握完整可运行的代码写法、bytesUploadFile 两种文件接收方式的选择依据,以及混用时不能同时声明 JSON Body 的 HTTP 协议层面的原因。

前置准备:安装 python-multipart

与单独使用 File(见 Request Files)或单独使用 Form(见 Form Data)一样,只要涉及文件上传或表单数据解析,就必须先安装 python-multipart 依赖,否则 FastAPI 无法解析 multipart/form-data 编码的请求体。

在本仓库的项目管理方式下,可直接用 uv 将其加入项目依赖:

$ uv add python-multipart

安装完成后,FastAPI 才能把请求体按 multipart/form-data 编码读取并分发到 File / Form 参数上。

导入 File 与 Form

在代码中,FileFormUploadFile 一样,都从 fastapi 顶层直接导出,示例参见 request_forms_and_files/tutorial001_an_py310.py

from typing import Annotated

from fastapi import FastAPI, File, Form, UploadFile

app = FastAPI()

从源码类层次看,Form 直接继承自 Body(见 fastapi/params.py),而 File 又继承自 Form(见 fastapi/params.py),因此三者在声明校验、别名(alias)、示例(examples)等配置能力上是同源相通的;File 本质上也是一种「特殊的表单字段」,这正是它能够与 Form 在同一种编码中共存的类设计基础。

在同一路径操作中定义 File 与 Form 参数

定义混合参数的方式与定义 BodyQuery 完全一致:给参数标注类型并调用 File()Form() 作为默认值 / Annotated 元数据即可,FastAPI 会自动识别并解析。

以同时接收两个文件和一个普通表单字段为例(推荐使用 Annotated 的写法):

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,
    }

如果项目仍使用更传统的默认值写法,效果完全等价(见 tutorial001_py310.py):

from fastapi import FastAPI, File, Form, UploadFile

app = FastAPI()


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

在上述端点中:

  • file: bytes:文件内容会被完整读入内存,成为一个 bytes 对象,因此可以直接用 len(file) 取得字节数。适合体积较小的文件。
  • fileb: UploadFile:以流式对象接收文件,可通过 fileb.content_type 访问客户端声明的媒体类型,也支持 await fileb.read() 等方式按需读取。适合体积较大的文件。
  • token: str:普通文本表单字段,用 Form() 声明。

请求发出后,这些文件与字段都会以 multipart/form-data 形式上传,FastAPI 会从请求体中解析并把它们注入到对应的函数参数中。

混合声明 bytes 与 UploadFile

教程中特别提到:可以在同一个端点中把部分文件声明为 bytes、另一部分声明为 UploadFile。上面的 filefileb 正是这种混合用法,二者互不冲突。选择依据主要是文件体积与处理方式:

声明类型 数据形态 适合场景
bytes 整个文件一次性读入内存 小文件,需要整体哈希、直接比较或存入内存
UploadFile 流式文件对象,可 await 异步读写 大文件,需要分块读取、落到磁盘或流式处理

客户端如何提交该请求

由于请求体编码是 multipart/form-data,客户端不能使用 application/json,而应使用标准的 HTTP 多部分表单上传。仓库配套测试 tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py 中用 TestClient 完整演示了正确提交方式:普通字段放进 data,文件放进 files

response = client.post(
    "/files/",
    data={"token": "foo"},                                  # 表单字段
    files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")},  # 文件
)

提交成功后,接口返回:

{
    "file_size": 14,
    "token": "foo",
    "fileb_content_type": "text/plain"
}

其中 file_size 直接等于所上传文件内容的字节长度,fileb_content_type 则取自已提交的 text/plain

该测试文件还覆盖了多种失败场景,全部断言返回 HTTP 422 及对应校验明细,可用作理解校验行为的依据:

  • 完全不提交任何数据时,filefilebtoken 全部报 missing / Field required
  • 只提交 token、不提交文件时,filefileb 报缺失;
  • 只提交文件、不提交 token 时,filebtoken 报缺失;
  • application/json 提交client.post("/files/", json={...}))时同样返回 422,因为端点期待的是 multipart/form-data 请求体。

测试对 /openapi.json 的断言(test_openapi_schema)也印证了这一点:该路径操作的 requestBody.content 只有 multipart/form-data 一种媒体类型,对应的 schema(Body_create_file_files__post)中 filefilebtoken 都是必填(required)的字符串类型字段,且文件字段带有 contentMediaType: application/octet-stream,OpenAPI 文档中会据此正确渲染为文件上传控件。

混用 File / Form 时为何不能同时声明 JSON Body

warning

你可以在同一个路径操作中声明多个 FileForm 参数,但不能同时声明期望以 JSON 接收的 Body 字段,因为此时请求体的编码是 multipart/form-data 而非 application/json

这一点并非 FastAPI 的限制,而是 HTTP 协议本身决定的:一个请求体只能采用一种内容编码方式。既然端点中出现了 File / Form 参数,FastAPI 就会把整个请求体按 multipart/form-data 解析,此时再期待某些字段以 JSON Body 形式出现自然无法成立。这一点与单独使用 Form 时的约束完全一致(见 Form Data 文档 中的同类 warning)。

因此,如果确有「既要 JSON 负载、又要文件」的需求,更合理的做法是拆成两个端点,或在单个 multipart/form-data 中把所有内容都声明为 Form / File 字段,而不是混用两种请求体编码。

小结

  • 需要「数据 + 文件」在同一个请求中上传时,把 FileForm 一起用在路径操作的参数列表里即可;
  • 记得先通过 uv add python-multipart(或 pip 等价命令)安装依赖;
  • 文件既可声明为 bytes(小文件整块读入),也可声明为 UploadFile(流式处理大文件),同一端点内可以混合使用;
  • 只要声明了 File / Form,请求体就是 multipart/form-data,不能再同时声明 JSON Body 字段——这是 HTTP 协议的行为,与框架无关;
  • 参考仓库中的完整可运行示例:tutorial001_an_py310.py,配套的行为与 OpenAPI 断言测试:test_tutorial001.py
登录后查看全文
热门项目推荐
相关项目推荐