system.text.json 默认拒绝循环引用并抛出异常,必须显式配置referencehandler.preserve或ignorecycles;前者保留完整对象图但json体积增大,后者静默截断导致关系链断裂。

System.Text.Json 默认拒绝循环引用,直接抛出 InvalidOperationException: A possible object cycle was detected;Newtonsoft.Json 默认同样报错,但提供更灵活的控制入口。两者都**不能靠忽略属性或静默截断来真正“解决”**——那只是掩盖问题,会导致反序列化后对象图断裂、EF Core 变更追踪失效、前端无法还原导航关系。
用 System.Text.Json 处理循环引用(.NET 6+)
必须显式启用引用处理,且序列化与反序列化选项必须严格一致,否则反序列化会失败。
-
ReferenceHandler.Preserve:生成带$id/$ref的 JSON,保留完整对象图,可双向导航,但 JSON 体积增大、结构变复杂;反序列化时若没配相同ReferenceHandler,会抛JsonException:“Cannot resolve reference '1'” -
ReferenceHandler.IgnoreCycles:跳过已见对象,不生成$ref,JSON 更小,但反序列化后child.Parent为null,关系链断裂 - 仅在 .NET 6 及以上支持;.NET 5 或更早版本即使设了也会被忽略,仍报错
- 示例:
var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, WriteIndented = true }; var json = JsonSerializer.Serialize(person, options); // 反序列化也必须用同一 options var restored = JsonSerializer.Deserialize<person>(json, options);</person>
用 Newtonsoft.Json 处理循环引用
它不依赖运行时版本,但配置项名称和行为细节容易混淆,尤其在升级项目时。
-
PreserveReferencesHandling.Objects是等效于ReferenceHandler.Preserve的安全选择;ReferenceLoopHandling.Ignore则是静默丢弃,和IgnoreCycles类似 - 必须同时设置
TypeNameHandling.Auto(或TypeNameHandling.Objects)才能正确还原多态类型;否则反序列化时可能创建基类实例而非实际子类 - 如果用了自定义
SerializationBinder,必须确保其BindToType能识别 JSON 中的$type字段,否则反序列化失败 - 示例:
var settings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, TypeNameHandling = TypeNameHandling.Auto }; var json = JsonConvert.SerializeObject(person, settings); var restored = JsonConvert.DeserializeObject<person>(json, settings);</person>
别踩这些坑:常见错误现象与真实后果
很多“能跑通”的配置其实埋着隐患,上线后才暴露。
- 只给
JsonSerializerOptions设DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,却没配ReferenceHandler→ 序列化仍崩,不是 null 导致的 - 用
[JsonIgnore]标记Parent属性 → 反序列化后child.Parent == null,EF Core 认为该字段未修改,SaveChanges 不触发关联更新 - Newtonsoft 中设了
PreserveReferencesHandling.Objects,但漏了TypeNameHandling→ 多态集合(如List<animal></animal>含Dog和Cat)反序列化全变成Animal实例 - 前后端共用同一套 DTO,但前端依赖
$id/$ref做局部刷新 → 后端换用IgnoreCycles后,前端 JS 解析时报错“cannot read property 'parent' of null”
选 Preserve 还是 IgnoreCycles?关键看下游是否需要完整对象图
这不是性能或写法偏好问题,而是数据契约问题。
- 如果你的 API 要被前端 Vue/React 组件消费,且组件靠
item.parent.id渲染面包屑或树形控件 → 必须用Preserve模式,并确保前端 JSON 解析器支持$ref(如json-cycle) - 如果只是导出日志、审计快照、或写入只读报表数据库 →
IgnoreCycles更轻量,避免冗余字段干扰分析 - EF Core + Web API 场景下,若 DTO 直接映射实体,又启用了延迟加载(
virtual导航属性),Preserve是唯一能保证反序列化后仍可访问child.Parent.Children的方式
ReferenceHandler = ReferenceHandler.Preserve,而是确认整个调用链——从 Controller 返回、经中间件、到客户端解析——每一步都理解并接受这个引用语义。一旦某环擅自“优化”掉 $ref,对象图就不可逆地碎了。











