FastAPI 同时接收表单字段与上传文件:File 与 Form 联合使用完整指南
本指南围绕 FastAPI 在同一个请求中同时接收普通表单字段与上传文件的经典场景展开,基于仓库内西班牙语文档 docs/es/docs/tutorial/request-forms-and-files.md 的主体脉络,并结合源码与测试用例深入剖析其底层机制。读完本文,你将掌握如何通过 File 与 Form 声明组合式 multipart 请求体、正确选择 bytes 与 UploadFile 两种文件接收方式、避免与 JSON Body 混用的协议陷阱,并能够写出可被 OpenAPI 自动描述并经过完整校验的“文件 + 数据”接口。
适用场景:一个请求同时携带文件与数据
在实际业务中,很多接口需要“文件 + 若干普通字段”同时提交,典型场景包括:
- 上传附件时同时提交备注说明(token、描述、标签等文本字段);
- 用户头像上传,同时提交昵称与签名;
- 批量导入文件时提交本次导入的分类或鉴权信息。
这类请求不能使用 JSON body,而必须使用 HTTP 的 multipart/form-data 编码——一个 body 里既包含文件 part,也包含普通表单字段 part。FastAPI 允许你在同一条 path operation 中同时声明 File 和 Form 参数,由框架自动完成 multipart 的解析、字段提取与类型转换。
注意:本文是接收表单字段、接收上传文件两篇指南的组合进阶版。若只接收文件、不接收普通字段,可只使用
File;若只接收普通文本字段(application/x-www-form-urlencoded),则单独使用Form。二者同屏出现时,才需要把请求体编码为multipart/form-data。
前置条件:安装 python-multipart
FastAPI 本身不实现 multipart 解析,而是委托给第三方库 python-multipart。无论你要接收上传文件(File)还是表单数据(Form),都必须先安装它,否则运行时解析会直接报错。官方文档推荐使用 uv 添加依赖:
$ uv add python-multipart
使用 pip 的等价命令为:
$ pip install python-multipart
该依赖的必要性在源码中有明确体现。在 fastapi/dependencies/utils.py 中,FastAPI 定义了 ensure_multipart_is_installed() 检查函数:它先尝试从 python_multipart 模块导入并断言版本大于 0.0.12,导入失败或版本过低时抛出形如 Form data requires "python-multipart" to be installed. 的明确错误,甚至能识别“装错包”(误装了 multipart 而非 python-multipart)的场景并给出卸载指引。因此建议始终安装官方推荐的 python-multipart。
导入 File 与 Form
在同一条路径操作函数中声明文件与表单字段,只需从 fastapi 导入 File、Form 以及文件类型 UploadFile:
from typing import Annotated
from fastapi import FastAPI, File, Form, UploadFile
app = FastAPI()
对应源码示例见 docs_src/request_forms_and_files/tutorial001_an_py310.py。
声明 File 与 Form 参数
声明方式与你为 Body 或 Query 声明参数完全一致——使用 Annotated 语法把 File()、Form() 作为类型的元数据:
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,
}
上述代码中出现了三种完全不同的声明,值得逐一拆解:
| 参数 | 类型声明 | 含义 |
|---|---|---|
file |
bytes = File() |
以 bytes 形式接收整个文件内容(一次性读入内存,适合小文件) |
fileb |
UploadFile = File() |
以 UploadFile 对象接收文件(支持流式读取、访问元信息,适合大文件) |
token |
str = Form() |
接收普通表单文本字段 |
可见,你可以在同一接口里混合使用 bytes 与 UploadFile 来接收不同文件——FastAPI 会为二者分别做不同的解析与转换,UploadFile 类型还额外暴露文件元信息(如 content_type)。
若不使用 Annotated 语法,也可以写成默认值形式(等价写法见 docs_src/request_forms_and_files/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,
}
提示:
Annotated写法是现代 FastAPI 推荐的首选方式,也更便于组合File/Form与校验约束(如max_length、pattern等,均可作为File()/Form()的参数传入)。仓库当前源码示例以py310命名(见文件名后缀),即面向 Python 3.10+ 的语法约定。
两种文件接收方式的选择建议
bytes:文件内容会被完整读入内存,代码里直接拿到bytes,使用最简单,但占用内存随文件大小线性增长。适用小文件、简单校验场景。对应上面len(file)即可直接取到字节数。UploadFile:底层是 Starlette 的UploadFile(在 fastapi/datastructures.py 中定义并增强),通过await file.read()分块读取、await file.close()释放资源,且带有filename、content_type等元信息属性。适用大文件或需要异步、流式处理的场景。示例中fileb.content_type直接取请求头声明的文件 MIME 类型。
在 async def 路径操作里可直接 await 文件读取方法;若使用普通 def 函数,FastAPI 会把 UploadFile 的读写放到线程池中执行,避免阻塞事件循环。
一个请求体中同时存在 Body(JSON) 字段?不行
原文档特别强调了一条警告,这里必须原样继承并讲透:
你可以在同一条 path operation 中声明多个
File和Form参数,但不能同时声明期望以 JSON 形式接收的Body字段,因为此时请求体将使用multipart/form-data编码,而非application/json。这不是 FastAPI 的限制,而是 HTTP 协议本身的规定。
原因是 HTTP 请求体在同一时刻只能有一种媒体类型编码。一旦请求体是 multipart/form-data,其中的“JSON body”根本无法被还原成整块 JSON 文档;同理,如果请求体是 application/json,也不存在 multipart 意义下的“文件 part”。因此当接口需要同时提交文件与普通数据时,普通数据应一律以 Form() 字段承载,而不是 Body()。
测试用例也验证了这一规则对请求方与响应方的约束——见 tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py:
test_post_body_json用client.post("/files/", json={...})发送纯 JSON,接口返回422校验失败,错误定位到缺失的file、fileb、token三个 body 字段;test_post_form_no_body完全不发送请求体,同样得到三个missing校验错误(loc均为["body", ...]),说明三个参数默认都是必填的。
这从测试层面印证了:声明了 File/Form 的接口,其请求契约就是 multipart 表单,任何非 multipart 或字段缺失的调用都会被统一拦截在 422 校验层。
底层机制:File 与 Form 如何驱动 multipart 解析
从源码结构看,File 与 Form 的关系非常清晰,二者共同构成了请求体解析分支的依据:
- fastapi/params.py 中的
Form类继承自Body,默认media_type为application/x-www-form-urlencoded,并暴露min_length、max_length、pattern、examples、description等丰富的声明参数; - fastapi/params.py 中的
File类又继承自Form,默认media_type被覆写为multipart/form-data,同时继承全部表单声明能力。
正因为 File 是 Form 的子类,请求分发层才能用一个统一判断覆盖“纯表单”与“表单 + 文件”两种场景。在 fastapi/routing.py 中,FastAPI 通过 isinstance(body_field.field_info, params.Form) 判断请求体是否为表单类型,若是则调用 await request.form() 解析 multipart/form 请求体(并注册异步关闭回调释放底层 SpooledTemporaryFile),否则才走 await request.body() 的普通请求体读取分支。
把上述类层级、默认媒体类型与测试里的 OpenAPI schema 断言(test_openapi_schema)放在一起,可以得到一条完整的证据链:
- 只要存在
File或Form参数,request.form()分支即被激活,请求体按 multipart 解析; - OpenAPI 生成的
requestBody.content只会是multipart/form-data(schema 为自动推导的Body_create_file_files__post,其中file、fileb的 JSON Schema 用contentMediaType: application/octet-stream描述二进制 part,token则是普通string); required列表自动包含所有无默认值的File/Form字段,这与 422 测试中三个字段全部必填的行为完全一致。
也就是说,从参数声明、运行时解析到文档生成,整条链路都由 File→Form→Body 的继承关系统一驱动,开发者只需声明参数,无需手写任何 multipart 解析逻辑。
完整的请求与响应示例
启动上面的应用后,可以用 curl 或任意 HTTP 客户端向 POST /files/ 发送 multipart 请求:
$ curl -X POST http://127.0.0.1:8000/files/ \
-F "file=@test.txt" \
-F "fileb=@testb.txt;type=text/plain" \
-F "token=foo"
仓库测试 tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py 中的 test_post_files_and_token 给出了等价的 TestClient 写法:通过 data={"token": "foo"} 传表单字段、files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")} 传文件 part,并断言响应为:
{
"file_size": 14,
"token": "foo",
"fileb_content_type": "text/plain"
}
其中 file_size 是 bytes 方式接收的文件字节数,fileb_content_type 则来自 UploadFile 的 content_type 元信息。同时该测试还覆盖了多种异常路径:缺失全部字段、缺失文件、携带了 token 但缺文件等情形都会得到结构化的 422 校验错误——这正是 FastAPI 在“文件 + 表单”接口上默认提供的契约校验能力。
小结
在 FastAPI 中处理“同一请求里既有文件又有普通字段”的需求,核心动作只有两步:
- 先通过
uv add python-multipart(或pip install python-multipart)安装 multipart 解析依赖; - 在同一条 path operation 中同时用
File()声明文件参数(小文件用bytes、大文件用UploadFile)、用Form()声明普通表单字段。
需要牢记的约束是:一旦声明了 File/Form,请求体编码即固定为 multipart/form-data,不要再试图混入期望 JSON 的 Body 字段——这是 HTTP 协议层面的硬性规则,而非框架缺陷。理解这一点后,再结合 OpenAPI 自动文档与 422 结构化校验,即可稳定地搭建出“上传文件 + 提交数据”的生产级接口。
延伸阅读
- 接收表单字段(Form):单独使用
Form处理application/x-www-form-urlencoded场景 - 接收上传文件(File):深入对比
bytes与UploadFile、多文件上传与List[UploadFile] - 表单字段模型(Form Models):使用 Pydantic 模型批量声明表单字段
- 请求体多参数(Body Multipart):理解
Body与表单/文件在请求体契约上的差异 - 示例源码:docs_src/request_forms_and_files/ 目录下的
tutorial001_py310.py与tutorial001_an_py310.py - 测试验证:tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py
- 实现原理:fastapi/params.py(
Form/File类定义)、fastapi/routing.py(multipart 请求体解析分支)、fastapi/dependencies/utils.py(python-multipart 依赖检查)
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 StartedRust0627
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