Newtonsoft.Json 如何用 GetObjectData 序列化并用默认构造函数还原循环引用?
结论
序列化和反序列化可以分别定制。不过,只实现 JsonConverter 并不能自动保留对象引用。CanRead = false 只表示读取时不调用这个转换器;它不会让 Json.NET 在写入时自动把转换器输出的对象内容转换成带 $id、$ref 的引用图。
问题中的对象关系是 Section → Tables → Table → Section。转换器遍历 SerializationInfo 时再次调用 serializer.Serialize 写出 Table.Section,会重新遇到原来的 Section,从而触发循环引用检查。此时转换器写出的是普通 JSON 属性,不能假定 Json.NET 会像处理内置 ISerializable 契约那样维护引用身份。
优先考虑:使用内置 ISerializable 契约
如果可以接受 SerializationInfo 构造函数,应保留 Json.NET 的 JsonISerializableContract,由 Json.NET 通过 GetObjectData 写出数据,并在读取时调用对应的 SerializationInfo 构造函数。配合 PreserveReferencesHandling.Objects,这是处理这类对象图最直接的方式。ConstructorHandling.AllowNonPublicDefaultConstructor 不会把 ISerializable 类型改成普通对象契约,也不能覆盖它专用的反序列化方式。
必须使用默认构造函数时
这需要实现一套完整的自定义引用格式,不能只把 GetObjectData 的条目逐项写入 JSON:
- 写入对象时,通过引用解析器登记对象。对象首次出现时输出
$id;再次遇到同一实例时输出$ref,并停止递归写出其成员。 - 读取时,遇到
$ref就解析已有实例;遇到新对象时,先调用非公开默认构造函数,并在读取成员前登记$id,再填充引用属性。 - 确保普通对象契约可以写入所需成员。对于
IReadOnlyList<ITable>,还要明确如何创建和填充实际列表。
这套自定义方案需要负责引用登记、读取顺序和集合处理。仅在 WriteJson 中循环调用 serializer.Serialize 并关闭读取端,无法完成这些步骤。如果不想维护自定义 $id/$ref 协议,可以定义普通 DTO 或代理类型,让 Json.NET 处理引用,再将结果映射回领域对象。
注意事项
TypeNameHandling.All 会在 JSON 中写入类型信息;只有在 JSON 来源可信,或已配置严格的类型绑定限制时才应启用。这个设置本身不能解决循环引用。具体的转换器实现还需结合所用的 Json.NET 版本验证,因为原文没有提供版本号或完整运行环境。
备注:内容仅供参考。
