深拷贝不能解决JSON序列化循环引用问题;需用自定义JSONEncoder拦截循环并替换为ID引用,或改用weakref、ID字段、分步API及json_tricks等库实现安全转换。

深拷贝本身能自动处理对象层级的循环引用(靠 memo 字典记录已拷贝对象 ID),但输出“清洗过的、无循环的干净 JSON 结构”是另一件事——JSON 标准不支持循环引用,json.dumps() 遇到循环会直接报错 ValueError: Circular reference detected。所以关键不是“深拷贝是否成功”,而是“如何把含循环的对象安全转成 JSON”。
先用 deep copy 断开原始循环(可选但推荐)
如果原始对象存在强循环引用(比如 A → B → A),直接 json.dumps(obj) 必败。此时可先用 copy.deepcopy() 得到一个逻辑等价但无共享引用的新对象——它内部仍可能有循环结构(如字典里嵌套自己),但 deepcopy 会确保不会因引用重复而崩溃;不过这步不能解决 JSON 序列化问题,只是为后续清洗提供更可控的起点。
- 仅当对象不含不可序列化字段(如锁、文件句柄)时,deepcopy 才能成功
- 若失败,说明对象本身设计就不支持序列化,需先清理状态(如剔除
_lock、_fd等字段) - deepcopy 后的对象仍需进一步处理才能转 JSON,因为它没改变结构上的循环
用自定义 JSONEncoder 过滤循环并替换为 ID 引用
最实用的方式是继承 json.JSONEncoder,在编码过程中检测并拦截循环引用,用唯一标识(如 id(obj) 或业务 ID)代替原对象。
- 重写
default()方法:对每个待序列化的对象,先查是否已在当前编码路径中出现过 - 维护一个栈或集合记录“正在编码中的对象 ID”,遇到重复就返回
{"$ref": id(obj)}而非递归展开 - 示例中常把循环节点替换成
{"$id": "123", "$ref": "123"}形式,便于前端或下游解析还原 - 避免用
str(id(obj))作 key——进程重启后失效;优先用对象自带的稳定 ID 字段(如obj.uid)
用 weakref 或 ID 懒加载提前规避循环结构
真正治本的方法不是“清洗”,而是从源头减少循环。很多循环依赖来自对象间双向强引用(如 parent ↔ children)。这类结构转 JSON 前必须打破。
- 把反向引用改为
weakref.ref(parent),这样 parent 被释放后 child 不会阻止其回收,也自然不会被 json.dumps() 遍历到 - 更稳妥的是只存 ID:例如
self.parent_id = parent.id,序列化时只输出 ID 字段;需要时再通过 ID 查库或缓存重建关系 - 对复杂图结构(如订单→用户→地址→订单),放弃一次性全量展开,改用分步 API 或 GraphQL 式按需请求
用第三方库辅助(如 json_tricks 或 circular-json)
如果项目允许引入依赖,json_tricks 支持保留类型、处理循环、甚至还原对象;circular-json(JS 生态)有 Python 移植版,原理类似自定义 Encoder,但封装更完整。
-
json_tricks.dumps(obj, allow_nan=False, primitives=True)可输出带$ref和$id的 JSON - 注意:这些库生成的 JSON 不是标准 JSON,下游系统需配套解析器才能还原循环语义
- 若只需“扁平、无循环、可读”的调试用 JSON,用
pprint.pformat()或dataclasses.asdict()(配合replace清洗)更轻量


















