FastAPI 实战:用 Base64 在 JSON 请求与响应中传输字节数据(val_json_bytes 与 ser_json_bytes)
JSON 协议本身只能承载 UTF-8 字符串,无法直接存放二进制内容。当你的 FastAPI 应用必须在 JSON 请求体或响应体中携带二进制数据(例如把一段图片、证书或文件字节嵌入结构化 JSON)时,标准的做法是对字节做 Base64 编码。本文基于 FastAPI 官方文档 JSON with Bytes as Base64 及其配套示例 docs_src/json_base64_bytes/tutorial001_py310.py,完整讲解如何用 Pydantic 的 val_json_bytes 与 ser_json_bytes 模型配置,让 FastAPI 自动完成 Base64 的解码入参与编码出参,并深入到源码层面说明 OpenAPI 文档是如何声明该字段的。
Base64 与文件上传的取舍:先想清楚是否真的需要 JSON 内嵌二进制
官方文档在正文一开始就给出了一条重要建议:优先考虑是否可以用文件上传/文件响应代替 Base64。
- 上传二进制数据:优先使用 FastAPI 的 Request Files(docs/en/docs/tutorial/request-files.md);
- 下发二进制数据:优先使用 Custom Response 中的 FileResponse。
原因很直接:
- JSON 只能包含 UTF-8 编码的字符串,原始字节(
bytes)无法直接放入 JSON 文档; - Base64 编码必然膨胀体积。它用 6 个比特编码 8 个比特,通常比原始二进制数据多出约 1/3 的字符量,因此一般情况下,Base64 内嵌 JSON 的传输效率低于直接传文件(multipart/form-data + 二进制流);
- 只有在确实必须把二进制数据放进 JSON、且无法改用文件的场景(例如与某个只接受纯 JSON 的第三方系统对接、数据中需要同时携带结构化字段与二进制字段)时,才建议使用 Base64。
输入方向:用 val_json_bytes 让 Pydantic 自动解码 Base64
在 Pydantic 模型中声明 bytes 类型字段后,通过 model_config 设置 val_json_bytes: "base64",Pydantic 在校验(validate)JSON 输入时会把该字段的 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}
以上代码来自完整示例 docs_src/json_base64_bytes/tutorial001_py310.py 的 DataInput 模型与 /data 的 POST 端点。
请求与响应示例
启动应用后访问 /docs,可以看到 data 字段被声明为期望 base64 编码的字节(见文首截图)。你可以发送如下请求:
{
"description": "Some data",
"data": "aGVsbG8="
}
aGVsbG8=是字符串hello的 Base64 编码。
Pydantic 会自动把该 Base64 字符串解码为字节,端点内拿到的是原始 bytes 对象 body.data;上例再 decode("utf-8") 得到文本,返回响应:
{
"description": "Some data",
"content": "hello"
}
对应的自动化测试在 tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py 中:POST /data 发送 data: "SGVsbG8sIFdvcmxkIQ=="(即 Hello, World! 的 Base64),断言响应为 {"description": "A file", "content": "Hello, World!"},验证了解码链路完整可用。
输出方向:用 ser_json_bytes 让 Pydantic 自动编码字节为 Base64
响应方向用另一个配置项 ser_json_bytes:当模型在序列化(serialize)JSON 响应时,Pydantic 会把 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)
该代码取自 tutorial001_py310.py。注意端点返回的是字节对象 data,而客户端实际收到的 JSON 是:
{
"description": "A plumbus",
"data": "aGVsbG8="
}
测试同样覆盖了这一行为(test_get_data):GET /data 的响应体中 data 字段断言为 Base64 字符串 "aGVsbG8="。
输入与输出双向:同一个模型同时处理请求和响应
如果一个端点既接收又返回带字节的数据,可以直接用同一个模型同时配置两个选项:
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
取自 tutorial001_py310.py。请求时 val_json_bytes 负责把 Base64 解码为 bytes,响应时 ser_json_bytes 再把 bytes 编码回 Base64,两端自动对称。测试 test_post_data_in_out 验证了发送 {"data": "SGVsbG8sIFdvcmxkIQ=="} 后原样收到 Base64 字符串。
源码级原理:OpenAPI 文档如何声明 Base64 字段
上面三节代码之所以在 /docs 中自动显示为 "expects base64 encoded bytes",是因为 FastAPI 在生成 JSON Schema 时对 bytes 字段做了专门处理。在 fastapi/_compat/v2.py 中,FastAPI 定义了 GenerateJsonSchema.bytes_schema 方法:
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
从这段源码可以看到几个关键实现事实:
bytes字段在 JSON Schema 中生成type: "string",并附带contentMediaType: "application/octet-stream";- FastAPI 会区分校验模式与序列化模式:生成请求体(validation)schema 时读取
val_json_bytes,生成响应体(serialization)schema 时读取ser_json_bytes; - 只有对应模式的配置值为
"base64"时,才会在 schema 上追加contentEncoding: "base64"标注。
这一点也被 OpenAPI 测试用例完整印证:test_openapi_schema 断言了 DataInput、DataOutput、DataInputOutput 三个模型的 data 字段 schema 均为:
{
"type": "string",
"contentEncoding": "base64",
"contentMediaType": "application/octet-stream",
"title": "Data"
}
也就是说,无论前端代码生成器、API 客户端还是人工查阅,都能从 OpenAPI 规范中明确得知该字段需要 Base64 编码,而不需要额外的文字说明。
使用建议与限制小结
结合文档与源码,可以归纳出如下实践要点:
- 默认不用 Base64:能上传/下载文件就用 Request Files 与 FileResponse,Base64 仅用于"必须内嵌 JSON"的场景;
- 入参与出参是两个独立开关:
val_json_bytes只影响 JSON 输入校验,ser_json_bytes只影响 JSON 输出序列化,可按需单独或同时启用(完整可运行示例见 docs_src/json_base64_bytes/tutorial001_py310.py); - 端点内拿到/给出的是
bytes:解码与编码全部由 Pydantic 在 JSON 边界上完成,业务代码始终操作原生bytes对象,无需手工base64.b64decode/b64encode; - 版本前提:
val_json_bytes/ser_json_bytes是 Pydantic v2 的模型配置项,当前仓库 pyproject.toml 中声明依赖pydantic>=2.9.0,因此本方案要求 Pydantic v2 环境; - 文档自动化:配置生效后,
contentEncoding: base64会自动出现在/openapi.json中(见 fastapi/_compat/v2.py 的bytes_schema实现),API 文档无需手工维护。
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
