FastAPI 进阶:在 JSON 中用 Base64 传输二进制数据(bytes 字段实战)
本篇指南讲解 FastAPI 中一个进阶但非常实用的场景:当你的接口必须接收和发送 JSON 数据、其中又需要携带二进制内容(bytes)时,如何利用 Pydantic 的 val_json_bytes 与 ser_json_bytes 配置,把二进制数据安全地以 base64 编码嵌入 JSON 请求体与响应体。读完本文,你将掌握 base64 方案与文件上传/下载方案的取舍原则、bytes 字段模型配置的完整写法,以及 FastAPI 源码中 OpenAPI Schema 是如何自动生成 contentEncoding: base64 声明的底层机制。
何时需要用 Base64 而不是文件
如果你的应用需要接收和发送 JSON 数据,但其中必须包含二进制数据,就可以把这些二进制数据编码为 base64 字符串来传输。
在选择方案之前,先评估是否可以直接使用 请求文件 来上传二进制数据、使用 自定义响应 – FileResponse 来下发二进制数据,而不是把二进制内容编码进 JSON。两者的取舍依据如下:
- JSON 只能包含 UTF-8 编码的字符串,因此它无法承载原始字节(raw bytes);
- Base64 可以把二进制数据编码成字符串,但代价是需要比原始二进制数据更多的字符(通常膨胀约 1/3),因此在传输效率上一般不如直接传文件;
- 只有当你确实必须把二进制数据内嵌在 JSON 中、且无法改用文件方案时,才使用 base64。
用 Pydantic bytes 字段接收输入数据
声明一个带 bytes 字段的 Pydantic 模型,并在模型配置中设置 val_json_bytes,即可告诉 Pydantic:在校验(validate)输入的 JSON 数据时使用 base64。校验过程中,base64 字符串会被自动解码为字节对象。
完整示例见 docs_src/json_base64_bytes/tutorial001_py310.py,其中接收端模型为:
from fastapi import FastAPI
from pydantic import BaseModel
class DataInput(BaseModel):
description: str
data: bytes
model_config = {"val_json_bytes": "base64"}
app = FastAPI()
@app.post("/data")
def post_data(body: DataInput):
content = body.data.decode("utf-8")
return {"description": body.description, "content": content}
启动应用后访问 /docs,Swagger UI 会展示字段 data 期望接收 base64 编码的字节:
此时可以发送如下请求:
{
"description": "Some data",
"data": "aGVsbG8="
}
提示:
aGVsbG8=就是字符串hello的 base64 编码。
Pydantic 会解码这个 base64 字符串,并在模型的 data 字段中把原始字节交给你。随后你会收到类似这样的响应:
{
"description": "Some data",
"content": "hello"
}
这里的关键点在于:端点函数拿到的 body.data 已经是解码后的 bytes 类型(示例中再用 .decode("utf-8") 转回字符串),base64 编解码完全由 Pydantic 在模型边界处自动完成,业务代码无需手动调用任何 base64 模块。
用 Pydantic bytes 字段输出数据
对于输出数据,可以在模型配置中使用 ser_json_bytes。Pydantic 在生成 JSON 响应时会把字节序列化(serialize)为 base64 字符串:
class DataOutput(BaseModel):
description: str
data: bytes
model_config = {"ser_json_bytes": "base64"}
@app.get("/data")
def get_data() -> DataOutput:
data = "hello".encode("utf-8")
return DataOutput(description="A plumbus", data=data)
响应体中 data 字段就会以 "aGVsbG8=" 这样的 base64 字符串形式返回。仓库中的测试 tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py 正是如此断言的:
def test_get_data(client: TestClient):
response = client.get("/data")
assert response.status_code == 200, response.text
assert response.json() == {"description": "A plumbus", "data": "aGVsbG8="}
测试同时覆盖了输入方向(发送 SGVsbG8sIFdvcmxkIQ== 解码为 Hello, World!),验证了这套机制在真实请求/响应链路中的端到端行为。
同一模型同时处理输入和输出
当然,你也可以配置同一个模型,让 base64 同时用于输入(校验)和输出(序列化):
class DataInputOutput(BaseModel):
description: str
data: bytes
model_config = {
"val_json_bytes": "base64",
"ser_json_bytes": "base64",
}
@app.post("/data-in-out")
def post_data_in_out(body: DataInputOutput) -> DataInputOutput:
return body
此时请求体里的 base64 字符串会被解码成 bytes 传入端点,端点把同一个模型对象返回后,bytes 又会以 base64 形式序列化进响应。上述测试文件中的 test_post_data_in_out 验证了这一回环:发送 "SGVsbG8sIFdvcmxkIQ==",响应体中 data 原样返回同一 base64 字符串。
源码视角:OpenAPI Schema 是怎么知道要声明 base64 的
一个值得注意的细节是:配置 val_json_bytes / ser_json_bytes 后,OpenAPI 文档会自动为 bytes 字段生成 contentEncoding: "base64" 和 contentMediaType: "application/octet-stream" 声明,/docs 界面因此能正确提示该字段期望 base64 字符串。从上面的测试快照(test_openapi_schema)可以看到生成的 Schema 片段:
"data": {
"type": "string",
"contentEncoding": "base64",
"contentMediaType": "application/octet-stream",
"title": "Data"
}
这一行为来自 FastAPI 对 Pydantic JSON Schema 生成器的定制覆盖,位于 fastapi/_compat/v2.py:
class GenerateJsonSchema(_GenerateJsonSchema):
def bytes_schema(self, schema: CoreSchema) -> JsonSchemaValue:
json_schema = {"type": "string", "contentMediaType": "application/octet-stream"}
bytes_mode = (
self._config.ser_json_bytes
if self.mode == "serialization"
else self._config.val_json_bytes
)
if bytes_mode == "base64":
json_schema["contentEncoding"] = "base64"
self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes)
return json_schema
从这段源码可以看出两个要点:
- FastAPI 重写了 Pydantic 的
bytes_schema方法,先按bytes的默认语义生成type: string+contentMediaType: application/octet-stream; - 然后根据当前模式(校验模式读
val_json_bytes、序列化模式读ser_json_bytes),当配置值为"base64"时追加contentEncoding: base64声明。
也就是说,Schema 声明与实际的数据编解码行为是同一份模型配置驱动的两面:同一组 model_config 既决定运行时如何解码/编码字节,也决定 OpenAPI 文档如何向调用方描述字段格式。
小结与适用边界
- 优先用文件:上传二进制用请求文件、下发二进制用
FileResponse; - JSON 无法承载原始字节,base64 是可嵌入 JSON 的通用编码,但字符膨胀使其通常不如直接传文件高效;
- 输入方向用
model_config = {"val_json_bytes": "base64"},输出方向用{"ser_json_bytes": "base64"},两者可同时配置在同一个模型上; - 配置生效后,
/docs中的 OpenAPI Schema 会自动带上contentEncoding: base64声明,调用方(包括自动生成的客户端)能据此正确编码请求体; - 参考实现:示例应用、端到端测试、Schema 生成定制。
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 StartedRust0623
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
