FastAPI 在 JSON 中传输二进制 Bytes:基于 Pydantic v2 的 Base64 编码实战(`val_json_bytes` 与 `ser_json_bytes`)
许多业务场景需要在 JSON 请求与响应中携带图片、文件内容、哈希摘要等二进制数据,但 JSON 本质上只允许 UTF-8 字符串。本文围绕 FastAPI 官方文档「JSON with Bytes as Base64」展开,讲解如何借助 Pydantic v2 的模型配置项 val_json_bytes / ser_json_bytes,让 FastAPI 在接收 JSON 时自动把 Base64 字符串解码为 bytes、在返回 JSON 时自动把 bytes 编码为 Base64 字符串。读完你可以在不接触原始字节的前提下,直接用类型注解完成「JSON ↔ 二进制」的双向转换,并获得规范的 OpenAPI 文档支持。
前提:JSON 里为什么需要 Base64?
FastAPI 应用经常需要「接收和发送 JSON 数据,但数据里又包含二进制内容」的场景。首先要理解一个硬性约束:
JSON 只能包含 UTF-8 编码的字符串,因此它无法承载原始字节(raw bytes)。
要想在 JSON 里携带二进制,就必须把原始字节编码成字符串,最通用的做法就是 Base64。
{
"description": "Some data",
"data": "aGVsbG8="
}
上面 data 字段的 aGVsbG8= 正是字符串 hello 的 Base64 编码——先取 hello 的 UTF-8 字节序列,再做 Base64 编码即可得到。对端拿到后先 Base64 解码、再按 UTF-8 解码,就还原出了原始内容。
先考虑 Files 方案:Base64 并非首选
在引入任何 JSON 编码方案之前,官方文档建议先评估你是否真的需要在 JSON 内嵌二进制,而非走「文件」通道:
- 上传二进制数据优先使用 Request Files(multipart 表单),详见 Request Files;
- 下发二进制数据优先使用自定义响应的
FileResponse,详见 Custom Response - FileResponse。
原因是 Base64 编码会带来明显的空间膨胀:编码后的字符串字符数通常多于原始二进制数据本身,效率天然低于直接传输文件。因此文档给出的指导原则很明确:
仅当确实必须把二进制数据放进 JSON、且无法改用文件方式时,才使用 Base64。
核心机制:Pydantic v2 的 val_json_bytes 与 ser_json_bytes
本方案的关键并非 FastAPI 自身的序列化代码,而是 Pydantic v2 提供的一对模型配置项。当前仓库在 pyproject.toml 中声明依赖 pydantic>=2.9.0,<3.0.0,这两个配置项正是在 v2 模型上生效的 JSON 行为开关:
| 配置项 | 作用阶段 | 行为 |
|---|---|---|
val_json_bytes |
输入校验(validation) | 告诉 Pydantic:当从 JSON 解析 bytes 类型字段时,按该取值解释输入;取 "base64" 时会把 Base64 字符串解码为原始 bytes |
ser_json_bytes |
输出序列化(serialization) | 告诉 Pydantic:当把 bytes 类型字段写入 JSON 响应时,按该取值编码;取 "base64" 时会把 bytes 编码为 Base64 字符串 |
两者取值均为 "base64"。声明方式是在 Pydantic 模型类中设置 model_config:
model_config = {"val_json_bytes": "base64"} # 仅输入
model_config = {"ser_json_bytes": "base64"} # 仅输出
model_config = {"val_json_bytes": "base64", "ser_json_bytes": "base64"} # 双向
文档配套的完整可运行示例位于 docs_src/json_base64_bytes/tutorial001_py310.py,下面按「输入 / 输出 / 双向」三种形态逐一拆解。
仅处理输入数据:解码 Base64 为 bytes
先看「只负责接收」的模型。它声明了一个 bytes 类型字段 data,并在模型配置中打开 val_json_bytes: "base64":
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}
当客户端 POST 下面这样的 JSON 请求体时:
{
"description": "Some data",
"data": "aGVsbG8="
}
Pydantic 会在校验阶段自动完成 aGVsbG8= 的 Base64 解码,把真正的原始字节塞进 body.data。路由函数里因此能直接调用 body.data.decode("utf-8") 得到明文内容,并把解码结果随响应返回:
{
"description": "Some data",
"content": "hello"
}
打开 FastAPI 自动生成的交互式文档 /docs,可以看到请求体模型里字段 data 已被标注为期望 Base64 编码的字节串;Swagger UI 中字段类型虽然显示为 string,但示例值形如 Base64 文本。下图即为官方文档配套的 Swagger UI 截图,data 字段在文档界面中被直观展示为可填入 Base64 字符串的字节字段:
更严谨的证据来自自动生成的 OpenAPI Schema。仓库测试 tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py 中对 /openapi.json 的断言显示,DataInput 的 data 字段在 schema 中体现为:
"data": {
"type": "string",
"contentEncoding": "base64",
"contentMediaType": "application/octet-stream",
"title": "Data"
}
contentEncoding: base64 与 contentMediaType: application/octet-stream 是对「二进制经 Base64 编码传输」的标准 OpenAPI 表达,客户端工具(如代码生成器)可以据此正确理解字段语义。
仅处理输出数据:把 bytes 序列化为 Base64
再看不负责接收、只负责返回的模型,它在模型配置中打开的是 ser_json_bytes: "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)
路由内部仍然以原生 bytes 组织数据("hello".encode("utf-8")),返回类型标注为 DataOutput。当 FastAPI 生成 JSON 响应时,Pydantic 会按 ser_json_bytes 的配置把 bytes 序列化为 Base64 字符串,客户端收到的响应是:
{
"description": "A plumbus",
"data": "aGVsbG8="
}
也就是说,业务代码全程只接触 bytes,Base64 编码细节完全由模型层负责,路由不需要任何手动 base64 编码调用。
输入输出共用同一模型:双向 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
这里的 POST /data-in-out 把请求体原样返回:进入时 data 从 Base64 解码为 bytes,返回时又从 bytes 编码回 Base64,JSON 线格式两侧完全一致。例如发送:
{
"description": "A plumbus",
"data": "SGVsbG8sIFdvcmxkIQ=="
}
响应会被原样回显为 data: "SGVsbG8sIFdvcmxkIQ=="(即 Hello, World! 的 Base64 编码)。
需要留意:同一模型在文档与 OpenAPI Schema 中只会生成一份 DataInputOutput 定义(见测试中对 /openapi.json 的断言),但由于 bytes 字段同时具备 contentEncoding: base64 信息,客户端在请求与响应两侧都能正确解析。
测试用例佐证:三种端点的实际行为
仓库用 TestClient 为上述示例编写了完整测试,位于 tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py,覆盖三条路径:
POST /data:提交data: "SGVsbG8sIFdvcmxkIQ==",断言响应content为解码后的Hello, World!(验证输入侧 Base64 → bytes 解码);GET /data:断言响应data为aGVsbG8=(验证输出侧 bytes → Base64 编码);POST /data-in-out:提交 Base64 数据后断言原样回显(验证同一模型双向转换)。
如果安装好依赖并准备就绪,可以在仓库根目录直接复现:
uv run pytest tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py
测试文件通过 needs_py310 标记按 Python 3.10+ 运行示例模块(示例源码命名为 tutorial001_py310.py,使用该写法即代表要求 Python 3.10+ 环境),同时 test_openapi_schema 用例把完整 OpenAPI 输出固化为快照,任何 schema 变化都会被测试捕获。
总结与使用建议
把二进制数据放进 FastAPI 的 JSON 请求 / 响应时,本方案的核心收益是零手工转换:
- 模型字段直接声明为
bytes,让类型系统准确表达二进制语义; - 通过
model_config中的val_json_bytes: "base64"让 Pydantic 在输入校验时自动把 Base64 解码为bytes; - 通过
model_config中的ser_json_bytes: "base64"让 Pydantic 在输出序列化时自动把bytes编码为 Base64; /docs与/openapi.json会以contentEncoding: base64、contentMediaType: application/octet-stream标准元数据标注字段,Swagger UI 自动生成可测试的请求体,代码生成工具也能正确理解字段。
最后重申文档给出的取舍建议:Base64 比原始二进制占用更多字符、传输效率更低,仅在确实无法改用 Request Files / FileResponse 传输文件的场景下使用;能用文件走文件的方案,往往更简单也更高效。
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
