bigywater.tech
Python · 75

json:自定义对象如何被序列化

json.dumps 遇到自定义对象只会抛 TypeError。要让它工作,得理解两个钩子——default 和 object_hook——到底在什么时候、按什么顺序被调用。这篇把每一次调用都记了下来。

3 次
一份数据里 set、User、date 各触发一次 default
由内到外
object_hook 的调用顺序:最内层的 dict 最先
tuple → list
往返后类型对不上,== 也是 False

做的事

一个含 set、自定义类、date 的字典,先 dumps 再 loads
实验脚本(节选) data = {"id": 7, "tags": {"b", "a"}, "owner": User("ann", datetime.date(2026, 1, 2)), "score": 9.5} def default(o): calls.append(repr(o)) # 记录每次调用 if isinstance(o, User): return {"__user__": True, "name": o.name, "joined": o.joined} if isinstance(o, datetime.date): return {"__date__": o.isoformat()} if isinstance(o, (set, frozenset)): return sorted(o) raise TypeError(...) # 别的类型继续报错 s = json.dumps(data, default=default)
实测 · 本机执行输出
TypeError: Object of type User is not JSON serializable

验证一:default 只在「不认识」时才被调用

实测 · 本机执行输出
   default({'a', 'b'})
   default(User('ann'))
   default(datetime.date(2026, 1, 2))
dumps → {"id": 7, "tags": ["a", "b"], "owner": {"__user__": true, "name": "ann", "joined": {"__date__": "2026-01-02"}}, "score": 9.5}

int、str、float 从头到尾没有触发 default;整个字典里只有三个对象触发了它,顺序是字典的遍历顺序(tags → owner → owner 里的 joined)。有一点容易忽略:default 返回的东西会被继续遍历——User 被转成 dict 后,dict 里的 joined 又触发了一次 default(date)。所以 default 不用递归处理,只需要处理「一层」。

验证二:object_hook 由内到外

实测 · 本机执行输出
   object_hook({'__date__': '2026-01-02'})
   object_hook({'__user__': True, 'name': 'ann', 'joined': datetime.date(2026, 1, 2)})
   object_hook({'id': 7, 'tags': ['a', 'b'], 'owner': User('ann'), 'score': 9.5})
loads → {'id': 7, 'tags': ['a', 'b'], 'owner': User('ann'), 'score': 9.5} | owner 类型: User | joined 类型: date
tags 还原成了: list ['a', 'b'] (set 变成了 list)

解析时每个 JSON 对象一旦解析完就调用一次 object_hook,所以最内层的日期 dict 最先被调用,等到调用 User 那一层时,joined 已经是 datetime.date 了。这是好事:你不需要自己递归。但也意味着 object_hook 每个 dict 都会碰到——包括最普通的、跟你的类型无关的 dict——所以必须先认出自己的标记(__user__),不认识就原样返回。另外注意 tags 变成了 list:JSON 里没有 set,标记都没留,还原不回去。

验证三:往返之后,类型不一定回得来

实测 · 本机执行输出
  tuple (1,2)        → [1, 2] → [1, 2]  相等? False
  int 键 {1:'a'}     → {"1": "a"} → {'1': 'a'}  相等? False
  bytes b'x'         → TypeError: Object of type bytes is not JSON serializable
  Decimal('1.10')    → TypeError: Object of type Decimal is not JSON serializable
  float('nan')       → NaN → nan  ← 输出了不是合法 JSON 的 NaN
  allow_nan=False    → ValueError: Out of range float values are not JSON compliant: nan
  大整数 2**70       → 1180591620717411303424 → 1180591620717411303424  相等? True
  loads('1e400')     → inf
  重复键 loads('{"a":1,"a":2}') → {'a': 2}   (后者覆盖前者)
  object_pairs_hook 看到全部: [('a', 1), ('a', 2)]

验证四:default 返回不认识的东西,会被拦下

实测 · 本机执行输出
default 返回自己 → ValueError: Circular reference detected
default 返回 [o] (里面还是同一个对象) → ValueError: Circular reference detected | __notes__ = ['when serializing list item 0', 'when serializing Node object'] | default 被调用 1 次

Python 用一个「正在序列化的对象」集合检测循环。default 返回自己,或返回一个里面还包着自己的 list,都会得到 Circular reference detected。3.14 里异常还带了 __notes__,标出是在序列化哪一层(list item 0 → Node object)时出的错,排查时很有用。

分步演示:一次 dumps、一次 loads 里所有钩子的调用

每一次调用都是上面实验里真实记录到的
default 与 object_hook 的调用顺序
当前这一次钩子调用

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7,标准库 json、datetime、decimal
    • 没有验证:C 加速的编码器与纯 Python 编码器在 default 调用次数上是否完全一致、JSONEncoder 子类写法与 default 参数的差异、超大数据下的性能
    京ICP备2026067502号-1京公网安备11010502063487号