FastAPI JSON Compatible Encoder 全解析:深入理解与应用 `jsonable_encoder()`
jsonable_encoder() 是 FastAPI 提供的一个核心序列化工具,负责把 Pydantic 模型、datetime、Decimal、Enum 等各类 Python 对象,递归转换为与 JSON 完全兼容的 Python 原生数据结构(dict、list、str 等)。本篇指南以官方教程《JSON Compatible Encoder》为骨架,结合 fastapi/encoders.py 的源码实现与 tests/test_jsonable_encoder.py 的测试用例,讲透它的使用场景、参数含义、支持的数据类型与内部递归机制。读完你不仅能熟练用它把对象安全地写入只接受 JSON 的数据库,还能理解 FastAPI 内部是如何借它完成响应序列化的。
为什么需要 jsonable_encoder?
Web 应用中经常遇到一类场景:需要把业务对象持久化到数据库,而某些数据库(或消息队列、缓存系统)只接受 JSON 兼容的数据。但你的业务对象往往并不 JSON 兼容:
- Pydantic 模型是一个带属性的对象,不是
dict,无法直接json.dumps(); datetime对象在 JSON 中并没有原生表达,需要转成字符串(例如 ISO 8601 格式);- 更复杂的类型,如
UUID、Decimal、Enum、Path、bytes、set等,也各有各的序列化需求。
Python 标准库 json.dumps() 面对这些类型会直接抛出 TypeError,无法胜任。FastAPI 因此提供了 jsonable_encoder() 函数:它接收任意对象,返回一个所有层级值都 JSON 兼容的 Python 数据结构。正如教程所指出的,典型用途就是"存入数据库之前先做一次转换"。
核心实战:把 Pydantic 模型存入只接受 JSON 的数据库
官方教程以一个只接受 JSON 兼容数据的 fake_db 为例演示了最经典的用法。完整示例见 docs_src/encoder/tutorial001_py310.py:
from datetime import datetime
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
fake_db = {}
class Item(BaseModel):
title: str
timestamp: datetime
description: str | None = None
app = FastAPI()
@app.put("/items/{id}")
def update_item(id: str, item: Item):
json_compatible_item_data = jsonable_encoder(item)
fake_db[id] = json_compatible_item_data
这个例子要重点说明两个关键转化,它们也是 jsonable_encoder 的核心能力:
- Pydantic 模型 →
dict:Item实例是带属性访问的对象,jsonable_encoder(item)会先把它展开成普通字典; datetime→str:timestamp字段是datetime对象,转换后成为 ISO 格式字符串(例如"2026-09-06T18:24:46"),这正是数据库中可直接存储、日后可直接解析的形式。
需要特别澄清的一点(教程中有专门说明):jsonable_encoder() 并不返回一个大的 JSON 字符串。它返回的是 Python 标准数据结构——一个值、子值都 JSON 兼容的 dict。因此调用它的结果可以直接交给标准库 json.dumps() 编码成字符串,也可以直接存入只认 JSON 兼容结构的数据库。
参数详解:从源码签名看全部可配置项
从源码看,jsonable_encoder 的定义 签名如下:
def jsonable_encoder(
obj: Any,
include: IncEx | None = None,
exclude: IncEx | None = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
custom_encoder: dict[Any, Callable[[Any], Any]] | None = None,
sqlalchemy_safe: bool = True,
) -> Any:
各参数含义如下表所示,它们大多会被透传给 Pydantic 模型的 model_dump():
| 参数 | 默认值 | 作用 |
|---|---|---|
obj |
(必填) | 待转换的任意输入对象 |
include |
None |
仅输出这些字段(Pydantic 语义),传入非集合可自动转 set |
exclude |
None |
排除这些字段(Pydantic 语义) |
by_alias |
True |
输出是否使用字段别名(Field(alias=...))。因为 API 里设别名就是为了让结果用别名,所以默认 True |
exclude_unset |
False |
排除「未被显式赋值、仅使用了默认值」的字段 |
exclude_defaults |
False |
排除「值与默认值相同」的字段(即使被显式赋值) |
exclude_none |
False |
排除值为 None 的字段 |
custom_encoder |
None |
自定义类型到编码函数的映射,优先级高于内置编码规则 |
sqlalchemy_safe |
True |
排除键名以 _sa 开头的字段(这是为兼容 SQLAlchemy 对象做的 hack,它们把内部状态存于 _sa 前缀属性,不应参与序列化) |
其中 include/exclude 在传入非 set/dict 类型(比如列表)时,源码会先执行 set(...) 归一化,见 encoders.py 的归一化逻辑,所以传 ["name"] 这种列表写法也完全可行。
内置支持的类型:一张编码映射表
实现通过一张模块级常量表 ENCODERS_BY_TYPE 声明了大量常见类型的"快速编码器",这是"即插即用"背后的机制:
| 输入类型 | 编码方式 | 说明 |
|---|---|---|
bytes |
o.decode() |
解码为字符串 |
datetime.date / datetime.datetime / datetime.time |
isoformat() |
转 ISO 8601 字符串(教程核心案例) |
datetime.timedelta |
td.total_seconds() |
转为总秒数 |
Decimal |
decimal_encoder |
无小数部分转 int,否则转 float;NaN/Infinity 保持语义(对应 float 的 nan/inf) |
Enum |
o.value |
取枚举成员的值 |
set / frozenset / deque / 生成器 |
list |
转成列表 |
UUID |
str |
转字符串 |
Path(pathlib) |
str |
转字符串路径 |
IPv4/IPv6 Address/Interface/Network |
str |
网络类型转字符串 |
NameEmail / Url / AnyUrl |
str |
Pydantic 网络类型 |
SecretStr / SecretBytes |
str |
密文类型转字符串 |
Pattern(正则) |
o.pattern |
取出正则源码字符串 |
Color / PyExtraColor(新旧 Pydantic 颜色类型) |
str |
颜色转字符串 |
注意 datetime.timedelta 与 Decimal 的处理并非简单转字符串,体现了"JSON 兼容"而非"一律字符串化"的设计取向。测试文件 tests/test_jsonable_encoder.py#L294-L313 专门验证了 Decimal 转 int/float 以及 NaN/Infinity 的行为。
源码级原理:递归转换的分支机制
jsonable_encoder 不是一个简单查表函数,而是一个对结构层层递归的处理器,对不同类型的对象走不同分支。梳理 encoders.py 的完整实现,主流程如下:
custom_encoder优先:若传入了自定义编码器,先按type(obj)精确匹配、再按isinstance子类匹配,命中的立即返回定制结果;- Pydantic v2 模型:调用
obj.model_dump(mode="json", ...)一次性完成字段筛选与序列化,再对结果递归执行jsonable_encoder; - dataclass:先
dataclasses.asdict(obj)展开成字典再递归,因此 dataclass 里的嵌套模型同样会被处理; Enum:返回obj.value;PurePath:转str;- JSON 基本类型(
str/int/float/None):原样返回; dict:逐个键值递归编码,同时执行include/exclude键集合过滤、_sa前缀剔除与exclude_none过滤,字典键也会被递归编码(见 encoders.py#L298-L306),所以非字符串键同样可以正确处理;- 序列类(
list/set/frozenset/GeneratorType/tuple/deque):逐元素递归,统一产出列表; - 查
ENCODERS_BY_TYPE表:先精确匹配type(obj),再通过预生成的按编码器聚类的类元组做isinstance匹配(覆盖子类场景,见generate_encoders_by_class_tuples); - 兜底:尝试
dict(obj)或vars(obj)把普通对象展开成字典再递归;两种都失败时抛出带原始异常列表的ValueError。
值得注意的是 Pydantic v1 模型实例会在第 10 步之前被拦截并抛出 PydanticV1NotSupportedError(见 encoders.py#L341-L345),测试用例 test_json_encoder_error_with_pydanticv1 也专门验证了这一行为。
深入使用:include / exclude 与字段筛选
实际开发中经常需要"只序列化部分字段",include 和 exclude 在任意嵌套层级都生效。下面结合测试用例演示:
class ModelWithDefault(BaseModel):
foo: str
bar: str = "bar"
bla: str = "bla"
model = ModelWithDefault(foo="foo", bar="bar")
jsonable_encoder(model) # {"foo": "foo", "bar": "bar", "bla": "bla"}
jsonable_encoder(model, exclude_unset=True) # {"foo": "foo", "bar": "bar"}
jsonable_encoder(model, exclude_defaults=True) # {"foo": "foo"}
jsonable_encoder(model, include={"foo"}) # {"foo": "foo"}
jsonable_encoder(model, exclude={"bla"}) # {"foo": "foo", "bar": "bar"}
这些断言来自 tests/test_jsonable_encoder.py#L187-L202。一个实用推论是:当你想"只把用户显式提交、而非默认值的字段写入更新记录"时,组合 exclude_unset=True 与 exclude_defaults=True 即可得到最小化的差异字典。此外,字段筛选能力同样作用于 dict 输入、dataclass 以及列表/字典的任意嵌套容器——测试中 [model]、{"key": model}、{"key": [model]} 结构均能正确继承筛选参数(见 test_encode_model_with_default_in_dict_and_list)。
高级用法:用 custom_encoder 覆盖内置序列化
当你对内置规则不满意时,可以通过 custom_encoder 传入 {类型: 编码函数} 映射。它优先于内置编码被查找。看测试中的例子(tests/test_jsonable_encoder.py#L219-L239):
class safe_datetime(datetime):
pass
instance = {"dt_field": safe_datetime.now()}
# 对子类 safe_datetime 单独定制,输出 "18:24:46" 这类时间字符串
jsonable_encoder(
instance,
custom_encoder={safe_datetime: lambda o: o.strftime("%H:%M:%S")},
)
# 对基类 datetime 定制,同样能命中子类实例
jsonable_encoder(
instance,
custom_encoder={datetime: lambda o: o.strftime("%H:%M:%S")},
)
从源码逻辑(先 type(obj) in custom_encoder,再遍历用 isinstance 匹配)可知,映射中声明的基类编码器也能覆盖到其子类实例,这为全局调整某一类对象的序列化方式提供了便利。同一测试文件还验证了自定义 Enum 编码器({MyEnum: custom_enum_encoder})同样生效,见 test_custom_enum_encoders。
FastAPI 内部也在用它:从路由到异常处理
教程末尾的 note 强调:jsonable_encoder 实际上是 FastAPI 内部用于转换数据的工具,只是它在很多其他场景中同样好用。这条声明可以从仓库源码得到印证——它是框架序列化链路的基石:
- 响应序列化:在 fastapi/routing.py 的 serialize_response 中,当响应没有
response_model时,返回内容会直接走jsonable_encoder(response_content),确保任何端点返回值都能被下游json.dumps处理; - 异常处理:fastapi/exception_handlers.py#L25 使用
jsonable_encoder(exc.errors())把校验错误的Error列表转成 JSON 兼容结构后写入 HTTP 响应体detail; - OpenAPI 生成:fastapi/openapi/utils.py#L679 以
jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True)生成完整 OpenAPI 文档,此外security定义、参数的example/examples等也都会先经过它处理(见 fastapi/openapi/utils.py); - SSE 数据载荷:在 fastapi/routing.py#L536 中,Server-Sent Events 的非 Pydantic 数据载荷同样由
json.dumps(jsonable_encoder(item.data))序列化。
由此可见,理解 jsonable_encoder 也相当于从序列化视角理解了 FastAPI"端点返回任意对象 → 输出 JSON 响应"这条内部主路径。
总结
jsonable_encoder() 是 FastAPI 中"通用对象 → JSON 兼容结构"的标准化解决方案:
- 用途:把 Pydantic 模型、dataclass、
datetime、UUID、Decimal、Enum、set等对象递归转成可用json.dumps()直接编码、或可直接存入只支持 JSON 数据库的 Python 原生结构; - 位置:定义于 fastapi/encoders.py,随
fastapi一并安装,可从fastapi.encoders导入; - 能力边界:返回
dict而非 JSON 字符串;内置类型映射表覆盖 20 余种常见类型;支持include/exclude/by_alias/exclude_unset/exclude_defaults/exclude_none/custom_encoder/sqlalchemy_safe全套精细控制; - 纵深价值:它既是教程推荐给开发者的数据库写入工具,也是 FastAPI 在响应序列化、异常处理与 OpenAPI 文档生成等环节的内部依赖,理解它有助于把握整个框架的序列化设计。
若要查看配套的可运行示例与行为契约,可分别参考 docs_src/encoder/tutorial001_py310.py 与 tests/test_jsonable_encoder.py。
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 StartedRust0624
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