FastAPI 在 JSON 中内嵌 Base64 编码的二进制数据:Pydanticbytes字段的校验与序列化全指南
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
JSON 只能承载 UTF-8 编码的字符串,无法直接存放原始字节,因此当应用需要"在 JSON 请求/响应里附带二进制数据(图片、文件片段、加密签名等)"时,通常先把字节编码为 Base64 字符串再放进 JSON。本文以 FastAPI 仓库中 docs_src/json_base64_bytes/tutorial001_py310.py 的完整可运行示例为核心,讲解如何借助 Pydantic 的val_json_bytes与ser_json_bytes模型配置,让 FastAPI 在输入时自动把 Base64 解码成bytes、在输出时自动把bytes序列化成 Base64,并给出仓库测试与 OpenAPI 生成结果的验证证据。读完你将掌握一套可直接复制的"JSON + Base64 二进制传输"方案,并理解它与文件上传/下载方案的取舍边界。
本文内容对应 FastAPI 官方文档 JSON with Bytes as Base64(中文成文,另一份同主题翻译版见 docs/ko/docs/advanced/json-base64-bytes.md)。
为什么需要在 JSON 里编码二进制:Base64 与文件方案的选择
先厘清一个核心约束:JSON 规范只允许 UTF-8 编码的字符串,没有"原始二进制"数据类型。因此想通过 JSON 传递一个 PNG 文件、一段任意字节,都必须先将字节编码成文本——最常用且具备互操作标准的就是 Base64。
不过,把二进制塞进 JSON 并不是首选方案:
- 上传二进制应优先使用 multipart 请求文件,参见教程 请求文件(Request Files);
- 下发二进制应优先使用流式响应,例如 自定义响应中的
FileResponse。
Base64 虽然能把字节编码成字符串,但它会比原始二进制多占用字符——每 3 个字节会被展开为 4 个 Base64 字符(开销约 1/3),且还需再叠加 JSON 自身的引号与转义开销,因此编码进 JSON 的传输方式在字节数上明显劣于普通文件流。只有当业务上确实必须在 JSON 内承载二进制、且无法改用文件传输时,才应当使用 Base64。典型的适用场景包括:需要把二进制嵌套进某个结构化业务报文、与第三方 JSON API 对接,或数据本身是必须放进 JSON body 的一小段字节(如签名、指纹、图标数据)。
一个跑得通的完整示例:三种模型与三个端点
仓库中随文档提供的可运行示例位于 docs_src/json_base64_bytes/tutorial001_py310.py(文件名后缀py310表明其依赖 Python 3.10+ 语法与 Pydantic v2 的model_config写法)。下面把它完整展开:
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可以看到,三个 Pydantic 模型都声明了data: bytes字段,区别只在于model_config:
DataInput:只配置val_json_bytes,用于接收(校验输入);DataOutput:只配置ser_json_bytes,用于返回(序列化输出);DataInputOutput:同时配置两者,同一模型既收又发。
应用搭建好后,/docs(Swagger UI)会自动展示接口文档。官方文档截图显示,POST /data的请求体说明中data字段会明确按 Base64 编码的 bytes 呈现,引导调用方提交形如aGVsbG8=的字符串:
输入侧:用val_json_bytes把 Base64 解码成bytes
model_config = {"val_json_bytes": "base64"}的含义是:当 Pydantic校验输入的 JSON 数据时,遇到bytes类型的字段,就把它当作 Base64 编码的字符串来解析,并在校验流程中自动完成 Base64 → 原始字节的解码。你的路径函数拿到的body.data已经是真正的bytes对象,无需手动调用base64.b64decode()。
对POST /data发送如下请求:
{ "description": "Some data", "data": "aGVsbG8=" }提示:
aGVsbG8=正是字符串hello的 Base64 编码。
Pydantic 会把data字段的 Base64 字符串解码为b"hello",于是示例端点body.data.decode("utf-8")得到"hello",返回:
{ "description": "Some data", "content": "hello" }这一行为在仓库测试 tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py 的test_post_data中得到了端到端验证:提交"SGVsbG8sIFdvcmxkIQ=="(即Hello, World!的 Base64),断言返回{"description": "A file", "content": "Hello, World!"}。
输出侧:用ser_json_bytes把bytes序列化成 Base64
model_config = {"ser_json_bytes": "base64"}作用于生成 JSON 响应的过程:Pydantic 在序列化模型时,会把bytes字段编码为 Base64 字符串后再写入 JSON,调用方拿到的是可安全放入 JSON 的文本。
示例中GET /data的返回类型注解为DataOutput,端点内部构造了DataOutput(description="A plumbus", data=b"hello"),经序列化后实际响应为:
{ "description": "A plumbus", "data": "aGVsbG8=" }仓库测试中的test_get_data恰好断言了这一结果(注意这里的data是 Base64 编码的"aGVsbG8=",而不是明文"hello"),说明输出配置确实生效。
同一模型兼顾输入与输出
如果同一个模型既要接收 Base64 编码的 JSON 输入、又要以 Base64 输出 JSON,只需在model_config里同时写入两个配置项(即上面的DataInputOutput)。POST /data-in-out端点直接回显收到的 body:
{ "description": "A plumbus", "data": "SGVsbG8sIFdvcmxkIQ==" }由于输入经val_json_bytes解码为bytes、输出又经ser_json_bytes重新编码为 Base64,响应中的data与请求保持一致,实现"收到什么、返回什么"的透明回显。test_post_data_in_out验证的正是这种往返一致性。
双端一致的 OpenAPI 文档与机器可读描述
把bytes字段交给 Pydantic 后,FastAPI 生成的 OpenAPI 也会随之变化。上述三个模型(DataInput、DataOutput、DataInputOutput)在/openapi.json中的data字段都被描述为(见仓库测试中的 OpenAPI snapshot 断言):
{ "type": "string", "contentEncoding": "base64", "contentMediaType": "application/octet-stream", "title": "Data" }也就是说,OpenAPI 3.1 会通过contentEncoding: base64与contentMediaType: application/octet-stream标准字段,向 Swagger UI、代码生成器等下游工具精确声明该字段承载的是 Base64 编码的字节流——这正是 Swagger UI 能向用户提示 Base64 输入样例、以及自动生成的客户端能正确编码二进制数据的原因,也让 API 契约具备机器可读的准确语义。
如何运行与验证(基于仓库现有测试)
该示例依赖 Python 3.10+(测试用needs_py310标记门控,参见 tests/utils.py)与 Pydantic v2 的model_config语法。可以直接用仓库的测试来验证全部行为:
pytest tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py -vtest_tutorial001.py内部通过importlib动态加载 docs_src/json_base64_bytes/tutorial001_py310.py,并使用 FastAPI 的TestClient依次覆盖四条路径:
test_post_data:Base64 请求输入 → 服务端解码出可读文本;test_get_data:bytes返回值 → Base64 序列化输出;test_post_data_in_out:同一模型完成解码与再编码的往返回显;test_openapi_schema:断言/openapi.json中三个模型及data字段的contentEncoding: base64等元数据。
若想本地手动体验,先安装fastapi与uvicorn,然后以该示例文件为入口启动:
pip install "fastapi" "uvicorn" uvicorn docs_src.json_base64_bytes.tutorial001_py310:app --reload随后打开http://127.0.0.1:8000/docs即可在 Swagger UI 上直观看到三个端点的 Base64 请求体提示,或直接访问http://127.0.0.1:8000/openapi.json查看生成的机器可读契约。
小结与适用边界
在 FastAPI 中把二进制数据放进 JSON,正确姿势是:声明bytes类型的 Pydantic 字段,并在model_config中按需启用val_json_bytes/ser_json_bytes为"base64"——输入侧自动解码、输出侧自动编码、两端共用同一模型也完全支持,OpenAPI 文档与客户端生成都会随之获得标准化的 Base64 语义。
但请始终记得文档反复强调的取舍:Base64 会让二进制膨胀约 1/3 且无法利用流式传输,在传输大文件类二进制时应优先考虑 Request Files 与 FileResponse;只有确实需要在 JSON 结构化报文内部携带小段二进制、且无法改用文件时,这一方案才是更优解。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考