FastAPI 在 JSON 中传输 Bytes:使用 Pydantic 的 `val_json_bytes` / `ser_json_bytes` 做 Base64 编解码
导读
当你的 FastAPI 应用必须以 JSON 承载二进制数据(图片片段、加密摘要、文件指纹等)时,单纯使用 bytes 字段会导致 JSON 序列化失败——因为 JSON 只能存放 UTF-8 字符串。本指南以官方进阶教程 JSON con Bytes como Base64 为主体,讲解如何借助 Pydantic v2 模型配置中的 val_json_bytes(输入校验)与 ser_json_bytes(输出序列化),让 bytes 字段在 JSON 请求与响应中自动以 Base64 字符串传输。读完你将掌握输入、输出、双向三种场景下的完整实现方案,并理解其底层校验与 OpenAPI 文档的表现形式。
为什么需要 Base64:先想清楚能否用文件
二进制数据要进入 JSON,首先会碰到一个硬性约束:JSON 只能包含 UTF-8 编码的字符串,无法承载原始字节(raw bytes)。Base64 可以把任意二进制数据编码成 ASCII 字符串,从而把它"塞进"JSON 的字符串槽位。
但在动手之前,请先评估是否真的需要这么做。官方文档明确建议优先考虑两条更"正统"的路径:
- 上行(上传):使用 Request Files(请求文件) 通过 multipart/form-data 上传二进制数据;
- 下行(下载):使用 Custom Response - FileResponse 直接以二进制流返回文件内容。
文档强调了两个关键理由:
- Base64 是一种 6-bit 到 8-bit 的映射,编码后的字符数会比原始二进制数据多出约三分之一,传输与存储效率通常低于直接传文件;
- 因此,只有在确实必须把二进制数据放进 JSON、且无法改用文件方案时,才应当使用 Base64。
完整示例代码
以下示例取自本仓库的 docs_src/json_base64_bytes/tutorial001_py310.py,它用三个 Pydantic 模型分别演示了"仅输入"、"仅输出"和"输入输出双向"三种 Base64 处理方式,并注册在三个端点上:
from fastapi import FastAPI
from pydantic import BaseModel
class DataInput(BaseModel):
description: str
data: bytes
model_config = {"val_json_bytes": "base64"}
class DataOutput(BaseModel):
description: str
data: bytes
model_config = {"ser_json_bytes": "base64"}
class DataInputOutput(BaseModel):
description: str
data: bytes
model_config = {
"val_json_bytes": "base64",
"ser_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}
@app.get("/data")
def get_data() -> DataOutput:
data = "hello".encode("utf-8")
return DataOutput(description="A plumbus", data=data)
@app.post("/data-in-out")
def post_data_in_out(body: DataInputOutput) -> DataInputOutput:
return body
这段代码展示了一个完整的 FastAPI 应用:启动后即可通过 POST /data、GET /data 和 POST /data-in-out 三个接口验证三种配置的差异。三个模型都声明了 data: bytes 字段,唯一区别在于 model_config 中开启的选项不同。
Pydantic bytes 用于输入数据校验(val_json_bytes)
如果你需要接收 JSON 请求体、并让其中的 Base64 字符串在校验阶段被解码成真实字节,就在模型配置中设置:
class DataInput(BaseModel):
description: str
data: bytes
model_config = {"val_json_bytes": "base64"}
配置了 val_json_bytes: "base64" 之后,Pydantic 在校验输入 JSON 时,会把 data 字段里的 Base64 字符串解码为原始 bytes,再按 bytes 类型继续处理。这意味着一份这样的请求可以被正确接收:
{
"description": "Some data",
"data": "aGVsbG8="
}
小提示:
aGVsbG8=正是字符串hello的 Base64 编码。
解码之后,FastAPI 会把原始的字节值赋给模型字段 body.data。若端点在处理时对字节做 decode("utf-8") 并返回文本,那么响应会是:
{
"description": "Some data",
"content": "hello"
}
在 /docs 中呈现为 Base64 编码的字节
当你访问该应用的交互式 API 文档 /docs(Swagger UI)时,可以看到 data 字段被标注为期望 Base64 编码的字节数据,示例值与编辑区域直接展示可提交的 Base64 字符串:
图中所见的字节类型标注来自 FastAPI 依据 OpenAPI 规范自动生成的 schema:开启 Base64 处理后,data 字段在 schema 中会被描述为 type: string,并附带 contentEncoding: base64 与 contentMediaType: application/octet-stream 两个声明。这一结构通过仓库中的测试被精确固化了下来——在 tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py 的 test_openapi_schema 里,/openapi.json 的快照断言了 DataInput 模型的 data 字段正是:
"data": {
"type": "string",
"contentEncoding": "base64",
"contentMediaType": "application/octet-stream",
"title": "Data"
}
该测试文件同时用 TestClient 验证了真实请求行为:向 /data 提交 {"description": "A file", "data": "SGVsbG8sIFdvcmxkIQ=="} 时,接口返回 {"description": "A file", "content": "Hello, World!"}——即 Base64 字符串已在服务端被还原为原始文本字节。这是上述配置端到端有效的直接证据。
Pydantic bytes 用于输出数据序列化(ser_json_bytes)
反过来,如果应用内部持有真实的 bytes(例如从文件或数据库读出的二进制内容),而你想在 JSON 响应中把字节安全地发给客户端,则在模型配置中设置:
class DataOutput(BaseModel):
description: str
data: bytes
model_config = {"ser_json_bytes": "base64"}
设置 ser_json_bytes: "base64" 后,Pydantic 在序列化输出数据生成 JSON 响应时,会自动把 bytes 字段编码为 Base64 字符串。示例如下:
@app.get("/data")
def get_data() -> DataOutput:
data = "hello".encode("utf-8")
return DataOutput(description="A plumbus", data=data)
注意这里 data = "hello".encode("utf-8") 是真实的字节对象,但由于响应模型声明了 ser_json_bytes,客户端收到的 JSON 中 data 会是被编码过的 Base64 字符串 "aGVsbG8="。这一点同样有测试背书:test_get_data 断言 GET /data 的响应精确等于 {"description": "A plumbus", "data": "aGVsbG8="}。
同一模型同时处理输入与输出
最方便的做法,是让同一个模型既负责接收 Base64 输入、又负责生成 Base64 输出,即同时开启两个配置项:
class DataInputOutput(BaseModel):
description: str
data: bytes
model_config = {
"val_json_bytes": "base64",
"ser_json_bytes": "base64",
}
当端点把请求体原样作为响应返回(或经过业务处理后再返回)时,双向的 Base64 编解码都会自动完成,无需手写任何编码/解码代码:
@app.post("/data-in-out")
def post_data_in_out(body: DataInputOutput) -> DataInputOutput:
return body
测试 test_post_data_in_out 证实了这一点:向 /data-in-out 提交 {"description": "A plumbus", "data": "SGVsbG8sIFdvcmxkIQ=="},接口原样返回相同 JSON——输入侧完成解码校验、输出侧完成重新编码,两个环节在请求-响应周期内无缝衔接。
底层实现机制小结
从配置到行为,可以将这套机制的要点归纳如下:
| 配置项 | 取值 | 作用阶段 | 行为 |
|---|---|---|---|
val_json_bytes |
"base64" |
输入校验(validate) | 将 JSON 请求中的 Base64 字符串解码为 bytes 赋给字段 |
ser_json_bytes |
"base64" |
输出序列化(serialize) | 将模型中的 bytes 编码为 Base64 字符串写入 JSON 响应 |
- 两者可以独立开启,也可以同时开启;取值均为字符串
"base64"。 - 开启后,FastAPI 生成的 OpenAPI schema 会为
bytes字段标注type: string+contentEncoding: base64+contentMediaType: application/octet-stream,交互式文档与客户端代码生成工具都能据此正确理解该字段(可对照 tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py 中的 schema 快照)。 - 模型配置采用 Pydantic v2 的
model_config字典写法,说明该能力建立在 Pydantic v2 的序列化/校验配置体系之上,本仓库使用的正是这一版本约定。
使用建议
- 优先文件方案:凡是二进制内容可以通过 multipart 上传(Request Files)或通过 FileResponse 下发,都应避免走 JSON,以换取更小的体积与更高的传输效率。
- 明确需要 JSON 内嵌二进制时再启用 Base64:例如需要把文件指纹、哈希、小段二进制随结构化元数据一并提交,或客户端协议约束为纯 JSON 时,才使用上文配置。
- 保持编解码声明收敛在模型层:把
val_json_bytes/ser_json_bytes配置在 Pydantic 模型上,业务代码拿到的始终是真正的bytes(输入)或给出真正的bytes(输出),编解码细节完全由模型配置接管,代码既清晰又不易出错。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
