FastAPI 表单与文件混合上传:在同一个 multipart/form-data 请求中声明 File 与 Form 参数
在 Web 开发中,经常遇到「上传文件 + 附带几个普通字段」的需求,例如上传头像时同时携带 token 做鉴权,或提交文件的同时附带描述性元数据。本文基于 FastAPI 官方教程 Request Forms and Files 展开讲解:如何在同一个路径操作中同时使用 File 与 Form 声明文件与表单字段参数,让客户端一次性以 multipart/form-data 编码把二者提交上来。读完本文你将掌握完整可运行的代码写法、bytes 与 UploadFile 两种文件接收方式的选择依据,以及混用时不能同时声明 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
在代码中,File、Form 与 UploadFile 一样,都从 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 参数
定义混合参数的方式与定义 Body、Query 完全一致:给参数标注类型并调用 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。上面的 file 与 fileb 正是这种混合用法,二者互不冲突。选择依据主要是文件体积与处理方式:
| 声明类型 | 数据形态 | 适合场景 |
|---|---|---|
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 及对应校验明细,可用作理解校验行为的依据:
- 完全不提交任何数据时,
file、fileb、token全部报missing/Field required; - 只提交
token、不提交文件时,file与fileb报缺失; - 只提交文件、不提交
token时,fileb与token报缺失; - 以
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)中 file、fileb 与 token 都是必填(required)的字符串类型字段,且文件字段带有 contentMediaType: application/octet-stream,OpenAPI 文档中会据此正确渲染为文件上传控件。
混用 File / Form 时为何不能同时声明 JSON Body
warning
你可以在同一个路径操作中声明多个
File和Form参数,但不能同时声明期望以 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 字段,而不是混用两种请求体编码。
小结
- 需要「数据 + 文件」在同一个请求中上传时,把
File与Form一起用在路径操作的参数列表里即可; - 记得先通过
uv add python-multipart(或 pip 等价命令)安装依赖; - 文件既可声明为
bytes(小文件整块读入),也可声明为UploadFile(流式处理大文件),同一端点内可以混合使用; - 只要声明了
File/Form,请求体就是multipart/form-data,不能再同时声明 JSONBody字段——这是 HTTP 协议的行为,与框架无关; - 参考仓库中的完整可运行示例:tutorial001_an_py310.py,配套的行为与 OpenAPI 断言测试:test_tutorial001.py。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00