如何用Newtonsoft.JSON序列化/反序列化带引用循环的ISerializable对象
解决ISerializable类循环引用时JSON序列化保留null属性的问题
问题背景
ISerializable虽已被弃用,但大量遗留IPC代码依赖该接口,当前正从.NET Remoting迁移到跨平台方案,需完全保留现有代码结构。现有代码组成:
- 无任何外部依赖的.NET Standard 2.0类库,包含
User和UserGroup两个实现ISerializable的类,二者存在引用循环(例如UserGroup.DefaultUser指向所属User,User又反向引用UserGroup) - .NET 8控制台程序,依赖Newtonsoft.Json,通过
InternalsVisibleTo访问类库内部类型
当前遇到的问题:设置ReferenceLoopHandling = ReferenceLoopHandling.Ignore时,序列化后的JSON直接缺失循环引用的DefaultUser属性;反序列化时,UserGroup的ISerializable构造函数调用info.GetValue(nameof(DefaultUser), typeof(User))会抛出SerializationException,提示"Member 'DefaultUser' was not found"。
需求:序列化生成的JSON必须明确包含"DefaultUser": null,且禁止在类库中引入Newtonsoft.Json相关特性,避免增加类库的外部依赖,仅通过自定义ContractResolver实现。
已尝试的无效方案:
- 设置
ReferenceLoopHandling.Serialize直接引发StackOverflowException - System.Text.Json的
ReferenceHandler.IgnoreCycles虽能生成带null的JSON,但不支持ISerializable,且存在额外类型处理问题,无法适配 - 了解到
PreserveReferencesHandling.Objects可解决循环引用,但会生成$id、$ref元数据,不符合明确输出null的需求
解决方案:自定义ContractResolver处理循环引用属性
核心思路:继承DefaultContractResolver,通过跟踪序列化过程中的对象引用,检测到循环引用时,强制将对应属性值序列化为null,而非直接省略该属性。
具体实现
- 编写自定义ContractResolver,实现循环引用跟踪与null值替换:
using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System.Collections.Generic; using System.Runtime.Serialization; using System.Threading; public class SerializableReferenceLoopContractResolver : DefaultContractResolver { // 用ThreadLocal保证多线程安全,跟踪当前正在序列化的对象集合 private readonly ThreadLocal<HashSet<object>> _serializingObjects = new ThreadLocal<HashSet<object>>(() => new HashSet<object>()); protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); // 仅处理实现ISerializable接口的类型属性 if (property.DeclaringType.GetInterface(nameof(ISerializable)) != null) { var originalProvider = property.ValueProvider; // 替换为自定义ValueProvider,处理循环引用返回null property.ValueProvider = new ReferenceLoopValueProvider(originalProvider, _serializingObjects); } return property; } private class ReferenceLoopValueProvider : IValueProvider { private readonly IValueProvider _innerProvider; private readonly ThreadLocal<HashSet<object>> _serializingObjects; public ReferenceLoopValueProvider(IValueProvider innerProvider, ThreadLocal<HashSet<object>> serializingObjects) { _innerProvider = innerProvider; _serializingObjects = serializingObjects; } public void SetValue(object target, object value) => _innerProvider.SetValue(target, value); public object GetValue(object target) { var value = _innerProvider.GetValue(target); // 检测到属性值是正在序列化的对象(循环引用),返回null if (value != null && _serializingObjects.Value.Contains(value)) { return null; } return value; } } protected override JsonContract CreateContract(Type objectType) { var contract = base.CreateContract(objectType); // 为ISerializable类型添加序列化前后的引用跟踪逻辑 if (objectType.GetInterface(nameof(ISerializable)) != null) { // 序列化开始时将对象加入跟踪集合 contract.OnSerializingCallbacks.Add((obj, context) => _serializingObjects.Value.Add(obj)); // 序列化结束后移除对象,避免内存泄漏 contract.OnSerializedCallbacks.Add((obj, context) => _serializingObjects.Value.Remove(obj)); } return contract; } }
- 序列化时配置使用该ContractResolver:
var serializerSettings = new JsonSerializerSettings { ContractResolver = new SerializableReferenceLoopContractResolver(), ReferenceLoopHandling = ReferenceLoopHandling.Ignore, // 可选,自定义逻辑已处理循环引用 Formatting = Formatting.Indented }; // 测试序列化示例 var alice = new User("Alice"); var adminGroup = new UserGroup("AdminGroup", alice); alice.Group = adminGroup; // 构造循环引用 var json = JsonConvert.SerializeObject(adminGroup, serializerSettings); // 生成的JSON会明确包含"DefaultUser": null,不会丢失属性
方案说明
- 完全无需修改类库代码,类库保持无外部依赖的纯净状态
- 通过ThreadLocal跟踪序列化对象,避免多线程场景下的冲突
- 反序列化时,
UserGroup的构造函数可正常获取到DefaultUser的null值,不会抛出SerializationException - 仅在控制台程序侧修改序列化配置,不影响遗留代码结构
补充说明
PreserveReferencesHandling.Objects虽能解决循环引用问题,但会生成额外元数据字段,不符合明确输出null的需求。而上述自定义ContractResolver方案精准匹配需求,同时保留了类库的无依赖特性。
内容的提问来源于stack exchange,提问作者bairog
相关产品推荐
相关产品推荐

