You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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,而非直接省略该属性。

具体实现

  1. 编写自定义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;
    }
}
  1. 序列化时配置使用该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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 17:29:52