症状
错误发生在序列化边界,且错误信息对问题描述得很精确:TypeError: Converting circular structure to JSON,堆栈指向 logger 或 res.json 内部的 JSON.stringify,而不是业务逻辑。对象本身没有错,只是形状不可序列化。用 util.inspect 打印它,会得到带 [Circular *1] 标记而不是异常的输出,第一个标记紧挨着闭合环路的那个属性——通常是双向 ORM 关联或 parent 指针。
诊断思路
第一步用 util.inspect 的 depth 选项找到环的位置;第二步检查可疑属性,若某个键的值就是外层对象本身,它就是 stringify 无法跨越的反向引用;第三步用更深的堆栈限制确认是哪个调用点在做序列化——logger、res.json 还是队列 payload,决定了谁应该收到投影对象。只在加载了 parent/sibling 关系的实体上才报错,是循环引用的典型特征。
根因
JSON 是树形格式,JSON.stringify 按深度优先遍历对象,无法编码指回祖先的节点。对象图里任何反向引用都会在序列化那一刻触发这个 TypeError,与数据本身是否正确无关。常见来源:order.customer 与 customer.orders 的双向关联、请求上下文对象被挂到子属性、树结构里每个节点的 parent 指针、缓存对象指回缓存条目自身。
修复与验证
API 响应一律序列化显式投影:手工挑出消费者需要的字段(如只取 customer.name),契约清晰且不再受模型关系影响。调试日志想要完整对象图时,用 util.inspect 或带 replacer 的安全版本,跳过已知的反向引用键。注意 replacer 版本只能给读日志的人用,绝不能发给客户端——静默丢弃循环虽然不抛错,却可能掩盖数据模型问题或泄漏多余字段。验证时重新请求之前失败的实体断言 200 且字段精确;让 logger 跑一遍带关联的实体确认不再抛错;再在测试里深扫序列化产物中的 Circular 标记,防止模型新增关系后隐患回归。
代码示例
TypeError: Converting circular structure to JSON
at JSON.stringify (<anonymous>)
at toPayload (src/routes/orders.js:58:17)
{ id: 7, customer: { name: 'Ada', orders: [ [Circular *1] ] } }
app.get('/orders/:id', async (req, res) => {
const order = await db.order.findById(req.params.id, { withCustomer: true });
res.json({
id: order.id,
customer: { name: order.customer.name }
});
});
const SAFE_KEYS = new Set(['parent', '_request', 'customer', 'owner']);
const safe = JSON.stringify(obj, (k, v) => (SAFE_KEYS.has(k) ? '[skipped]' : v), 2);