首页
/ FastAPI 同时接收表单字段与上传文件:File 与 Form 联合使用完整指南

FastAPI 同时接收表单字段与上传文件:File 与 Form 联合使用完整指南

2026-09-07 10:19:48作者:瞿蔚英Wynne

本指南围绕 FastAPI 在同一个请求中同时接收普通表单字段与上传文件的经典场景展开,基于仓库内西班牙语文档 docs/es/docs/tutorial/request-forms-and-files.md 的主体脉络,并结合源码与测试用例深入剖析其底层机制。读完本文,你将掌握如何通过 FileForm 声明组合式 multipart 请求体、正确选择 bytesUploadFile 两种文件接收方式、避免与 JSON Body 混用的协议陷阱,并能够写出可被 OpenAPI 自动描述并经过完整校验的“文件 + 数据”接口。

适用场景:一个请求同时携带文件与数据

在实际业务中,很多接口需要“文件 + 若干普通字段”同时提交,典型场景包括:

  • 上传附件时同时提交备注说明(token、描述、标签等文本字段);
  • 用户头像上传,同时提交昵称与签名;
  • 批量导入文件时提交本次导入的分类或鉴权信息。

这类请求不能使用 JSON body,而必须使用 HTTP 的 multipart/form-data 编码——一个 body 里既包含文件 part,也包含普通表单字段 part。FastAPI 允许你在同一条 path operation 中同时声明 FileForm 参数,由框架自动完成 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 导入 FileForm 以及文件类型 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 参数

声明方式与你为 BodyQuery 声明参数完全一致——使用 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() 接收普通表单文本字段

可见,你可以在同一接口里混合使用 bytesUploadFile 来接收不同文件——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_lengthpattern 等,均可作为 File()/Form() 的参数传入)。仓库当前源码示例以 py310 命名(见文件名后缀),即面向 Python 3.10+ 的语法约定。

两种文件接收方式的选择建议

  • bytes:文件内容会被完整读入内存,代码里直接拿到 bytes,使用最简单,但占用内存随文件大小线性增长。适用小文件、简单校验场景。对应上面 len(file) 即可直接取到字节数。
  • UploadFile:底层是 Starlette 的 UploadFile(在 fastapi/datastructures.py 中定义并增强),通过 await file.read() 分块读取、await file.close() 释放资源,且带有 filenamecontent_type 等元信息属性。适用大文件或需要异步、流式处理的场景。示例中 fileb.content_type 直接取请求头声明的文件 MIME 类型。

async def 路径操作里可直接 await 文件读取方法;若使用普通 def 函数,FastAPI 会把 UploadFile 的读写放到线程池中执行,避免阻塞事件循环。

一个请求体中同时存在 Body(JSON) 字段?不行

原文档特别强调了一条警告,这里必须原样继承并讲透:

你可以在同一条 path operation 中声明多个 FileForm 参数,但不能同时声明期望以 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_jsonclient.post("/files/", json={...}) 发送纯 JSON,接口返回 422 校验失败,错误定位到缺失的 filefilebtoken 三个 body 字段;
  • test_post_form_no_body 完全不发送请求体,同样得到三个 missing 校验错误(loc 均为 ["body", ...]),说明三个参数默认都是必填的。

这从测试层面印证了:声明了 File/Form 的接口,其请求契约就是 multipart 表单,任何非 multipart 或字段缺失的调用都会被统一拦截在 422 校验层。

底层机制:File 与 Form 如何驱动 multipart 解析

从源码结构看,FileForm 的关系非常清晰,二者共同构成了请求体解析分支的依据:

  • fastapi/params.py 中的 Form 类继承自 Body,默认 media_typeapplication/x-www-form-urlencoded,并暴露 min_lengthmax_lengthpatternexamplesdescription 等丰富的声明参数;
  • fastapi/params.py 中的 File 类又继承自 Form,默认 media_type 被覆写为 multipart/form-data,同时继承全部表单声明能力。

正因为 FileForm 的子类,请求分发层才能用一个统一判断覆盖“纯表单”与“表单 + 文件”两种场景。在 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)放在一起,可以得到一条完整的证据链:

  1. 只要存在 FileForm 参数,request.form() 分支即被激活,请求体按 multipart 解析;
  2. OpenAPI 生成的 requestBody.content 只会是 multipart/form-data(schema 为自动推导的 Body_create_file_files__post,其中 filefileb 的 JSON Schema 用 contentMediaType: application/octet-stream 描述二进制 part,token 则是普通 string);
  3. required 列表自动包含所有无默认值的 File/Form 字段,这与 422 测试中三个字段全部必填的行为完全一致。

也就是说,从参数声明、运行时解析到文档生成,整条链路都由 FileFormBody 的继承关系统一驱动,开发者只需声明参数,无需手写任何 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_sizebytes 方式接收的文件字节数,fileb_content_type 则来自 UploadFilecontent_type 元信息。同时该测试还覆盖了多种异常路径:缺失全部字段、缺失文件、携带了 token 但缺文件等情形都会得到结构化的 422 校验错误——这正是 FastAPI 在“文件 + 表单”接口上默认提供的契约校验能力。

小结

在 FastAPI 中处理“同一请求里既有文件又有普通字段”的需求,核心动作只有两步:

  1. 先通过 uv add python-multipart(或 pip install python-multipart)安装 multipart 解析依赖;
  2. 在同一条 path operation 中同时用 File() 声明文件参数(小文件用 bytes、大文件用 UploadFile)、用 Form() 声明普通表单字段。

需要牢记的约束是:一旦声明了 File/Form,请求体编码即固定为 multipart/form-data,不要再试图混入期望 JSON 的 Body 字段——这是 HTTP 协议层面的硬性规则,而非框架缺陷。理解这一点后,再结合 OpenAPI 自动文档与 422 结构化校验,即可稳定地搭建出“上传文件 + 提交数据”的生产级接口。

延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388