FastAPI 同时接收文件与表单数据:File 与 Form 参数混合使用实战指南
在构建上传类 API 时,一个常见需求是:同一次 HTTP 请求既要携带文件(图片、附件、音视频),又要携带若干表单字段(如 token、分类、备注等)。FastAPI 通过 File 与 Form 两个参数声明器支持这种混合场景:把文件参数和表单字段统一声明在同一个路径操作函数中,FastAPI 会将整个请求体按 multipart/form-data 编码解析。读完本篇,你将掌握如何在 FastAPI 中同时声明 File 与 Form 参数、理解 bytes 与 UploadFile 两种文件类型的差异、了解 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_info 是 Form 实例(File 是 Form 的子类,因此同样命中),就会调用 ensure_multipart_is_installed()。也就是说,缺失依赖会在应用构建路由时报错,而不是等到第一次请求才失败。
导入 File 和 Form
示例代码来自仓库中的 docs_src/request_forms_and_files/tutorial001_an_py310.py。首先从 fastapi 中导入 File、Form 以及 UploadFile:
from fastapi import FastAPI, File, Form, UploadFile
在 fastapi/params.py 中可以确认三者的类继承关系:Form 继承自 Body(fastapi/params.py),而 File 又继承自 Form(fastapi/params.py)。这个继承链决定了两件事:
- 参数能力一致:
File和Form都能接收alias、title、description、min_length、pattern等与Body/Query相同的一套校验参数,声明方式和声明 JSON Body 或查询参数时完全一致; - 媒体类型不同:
Form的默认media_type是application/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,其中 file、fileb 是文件部分(part),token 是普通表单字段部分,服务端在同一个函数签名里分别接收它们。
文件参数可以选择 bytes 或 UploadFile 两种类型。 上例刻意演示了两种并存:
file: Annotated[bytes, File()]:文件内容被一次性读入内存并作为bytes传给函数,用len(file)即可得到字节大小;fileb: Annotated[UploadFile, File()]:得到 UploadFile 对象,可异步分块读写,并携带filename、size、content_type、headers等元数据。
UploadFile 在 fastapi/datastructures.py 中继承自 Starlette 的 UploadFile,属性定义非常清晰:file(标准 Python 文件对象,同步访问用)、filename(原始文件名)、size(字节大小)、headers(该 part 的请求头)、content_type(如 text/plain、image/png)。它的 read/write/seek/close 方法都是 async 的,底层通过线程池执行,因此适合在 async def 路径操作中分块读取大文件,避免阻塞事件循环。
警告:不能与 JSON Body 字段混用
你可以声明多个 File 和 Form 参数,但不能同时声明期望以 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 原样返回,UploadFile 的 content_type 准确保留了客户端声明的 text/plain。
缺失字段的 422 校验:测试还验证了三种失败场景,均返回 422,loc 统一指向 body 下的具体字段名(file、fileb、token):
- 不发送任何数据时,三个字段全部
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 组件中,file 与 fileb 被建模为带 "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编码,不能与 JSONBody字段混用,这是 HTTP 协议层面的约束;- 生成的 OpenAPI 会将整个请求体建模为单一的
multipart/form-data请求体,文件字段带contentMediaType: application/octet-stream,便于文档与客户端生成工具正确理解接口。
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