C# 序列化时的JSON.NET StackOverflowException

C# 序列化时的JSON.NET StackOverflowException,c#,serialization,json.net,stack-overflow,C#,Serialization,Json.net,Stack Overflow,我的C#程序正在运行StackOverflowException,当我尝试序列化具有类似结构的对象时,如下所示: 对象具有相互引用的成员 无法尝试捕获(idk为什么) 如果计数设置为低于6500(可能因机器而异),则成功序列化 示例代码如下: class Chacha { public Chacha NextChacha { get; set; } } public static readonly JsonSerializerSettings Settings = new J

我的C#程序正在运行StackOverflowException,当我尝试序列化具有类似结构的对象时,如下所示:

  • 对象具有相互引用的成员
  • 无法尝试捕获(idk为什么)
  • 如果计数设置为低于6500(可能因机器而异),则成功序列化
示例代码如下:

class Chacha
{
    public Chacha NextChacha { get; set; }
}    
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All,
    PreserveReferencesHandling = PreserveReferencesHandling.Objects,
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};

static void Main(string[] args)
{
        int count = 15000;

        Chacha[] steps = new Chacha[count];
        steps[0] = new Chacha();

        for (int i = 1; i < count; i++)
        {
            steps[i] = new Chacha();
            steps[i-1].NextChacha = steps[i];
        }

        string serSteps = JsonConvert.SerializeObject(steps, Settings);
}
Chacha类
{
公共Chacha nextchachacha{get;set;}
}    
公共静态只读JsonSerializerSettings设置=新JsonSerializerSettings
{
TypeNameHandling=TypeNameHandling.All,
PreserveReferencesHandling=PreserveReferencesHandling.Objects,
ReferenceLoopHandling=ReferenceLoopHandling.Ignore
};
静态void Main(字符串[]参数)
{
整数计数=15000;
Chacha[]步数=新Chacha[计数];
步骤[0]=新的Chacha();
对于(int i=1;i
JSON.NET版本为:9.0.1
.NET Framework:4.5.2
如何序列化此结构的任何解决方案?


欢迎任何帮助或建议。谢谢

之所以出现stackoverflow异常,是因为Json.NET是一个递归的单过程树或图序列化程序,启用后,它总是序列化每个对象的第一次出现。您已经构建了15000元素
Chacha[]
数组,因此第一个条目是包含所有其他顺序链接项的链接列表的标题。Json.NET将尝试通过15000级递归将其序列化到15000级的嵌套Json对象,并在过程中溢出堆栈

因此,您需要做的是将整个链接表作为JSON数组只写在列表的开头。然而,不幸的是,Json.NET也是一个基于契约的序列化程序,这意味着无论嵌套深度如何,只要遇到给定类型的对象,它都会尝试编写相同的属性。因此,将
Chacha[]nextchachahalist
属性添加到
Chacha
对象没有帮助,因为它将在每个级别上写入。相反,有必要创建一个相当复杂的程序,以线程安全的方式跟踪序列化深度,并且只在顶层写入链接列表。下面是一个技巧:

class ChachaConverter : LinkedListItemConverter<Chacha>
{
    protected override bool IsNextItemProperty(JsonProperty member)
    {
        return member.UnderlyingName == "NextChacha"; // Use nameof(Chacha.NextChacha) in latest c#
    }
}

public abstract class LinkedListItemConverter<T> : JsonConverter where T : class
{
    const string refProperty = "$ref";
    const string idProperty = "$id";
    const string NextItemListProperty = "nextItemList";

    [ThreadStatic]
    static int level;

    // Increments the nesting level in a thread-safe manner.
    int Level { get { return level; } set { level = value; } }

    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    protected abstract bool IsNextItemProperty(JsonProperty member);

    List<T> GetNextItemList(object value, JsonObjectContract contract)
    {
        var property = contract.Properties.Where(p => IsNextItemProperty(p)).Single();
        List<T> list = null;
        for (var item = (T)property.ValueProvider.GetValue(value); item != null; item = (T)property.ValueProvider.GetValue(item))
        {
            if (list == null)
                list = new List<T>();
            list.Add(item);
        }
        return list;
    }

    void SetNextItemLinks(object value, List<T> list, JsonObjectContract contract)
    {
        var property = contract.Properties.Where(p => IsNextItemProperty(p)).Single();
        if (list == null || list.Count == 0)
            return;
        var previous = value;
        foreach (var next in list)
        {
            if (next == null)
                continue;
            property.ValueProvider.SetValue(previous, next);
            previous = next;
        }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        using (new PushValue<int>(Level + 1, () => Level, (old) => Level = old))
        {
            writer.WriteStartObject();

            if (serializer.ReferenceResolver.IsReferenced(serializer, value))
            {
                writer.WritePropertyName(refProperty);
                writer.WriteValue(serializer.ReferenceResolver.GetReference(serializer, value));
            }
            else
            {
                writer.WritePropertyName(idProperty);
                writer.WriteValue(serializer.ReferenceResolver.GetReference(serializer, value));

                var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());

                // Write the data properties (if any).
                foreach (var property in contract.Properties
                    .Where(p => p.Readable && !p.Ignored && (p.ShouldSerialize == null || p.ShouldSerialize(value))))
                {
                    if (IsNextItemProperty(property))
                        continue;
                    var propertyValue = property.ValueProvider.GetValue(value);
                    if (propertyValue == null && serializer.NullValueHandling == NullValueHandling.Ignore)
                        continue;
                    writer.WritePropertyName(property.PropertyName);
                    serializer.Serialize(writer, propertyValue);
                }

                if (Level == 1)
                {
                    // Write the NextItemList ONLY AT THE TOP LEVEL
                    var nextItems = GetNextItemList(value, contract);
                    if (nextItems != null)
                    {
                        writer.WritePropertyName(NextItemListProperty);
                        serializer.Serialize(writer, nextItems);
                    }
                }
            }
            writer.WriteEndObject();
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var jObject = JObject.Load(reader);

        // Detach and process $ref
        var refValue = (string)jObject[refProperty].RemoveFromLowestPossibleParent();
        if (refValue != null)
        {
            var reference = serializer.ReferenceResolver.ResolveReference(serializer, refValue);
            if (reference != null)
                return reference;
        }

        // Construct the value
        var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(existingValue == null ? typeof(T) : existingValue.GetType());
        T value = (existingValue as T ?? (T)contract.DefaultCreator());

        // Detach and process $id
        var idValue = (string)jObject[idProperty].RemoveFromLowestPossibleParent();
        if (idValue != null)
        {
            serializer.ReferenceResolver.AddReference(serializer, idValue, value);
        }

        // Detach the (possibly large) list of next items.
        var nextItemList = jObject[NextItemListProperty].RemoveFromLowestPossibleParent();

        // populate the data properties (if any)
        serializer.Populate(jObject.CreateReader(), value);

        // Set the next item references
        if (nextItemList != null)
        {
            var list = nextItemList.ToObject<List<T>>(serializer);
            SetNextItemLinks(value, list, contract);
        }

        return value;
    }
}

public struct PushValue<T> : IDisposable
{
    Action<T> setValue;
    T oldValue;

    public PushValue(T value, Func<T> getValue, Action<T> setValue)
    {
        if (getValue == null || setValue == null)
            throw new ArgumentNullException();
        this.setValue = setValue;
        this.oldValue = getValue();
        setValue(value);
    }

    #region IDisposable Members

    // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
    public void Dispose()
    {
        if (setValue != null)
            setValue(oldValue);
    }

    #endregion
}

public static class JsonExtensions
{
    public static JToken RemoveFromLowestPossibleParent(this JToken node)
    {
        if (node == null)
            return null;
        var contained = node.AncestorsAndSelf().Where(t => t.Parent is JContainer && t.Parent.Type != JTokenType.Property).FirstOrDefault();
        if (contained != null)
            contained.Remove();
        // Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
        if (node.Parent is JProperty)
            ((JProperty)node.Parent).Value = null;
        return node;
    }
}
为3个项目的数组生成以下JSON:

{
  "$type": "Question41828014.Chacha[], Tile",
  "$values": [
    {
      "$id": "1",
      "Data": 0,
      "nextItemList": {
        "$type": "System.Collections.Generic.List`1[[Question41828014.Chacha, Tile]], mscorlib",
        "$values": [
          {
            "$id": "2",
            "Data": 1
          },
          {
            "$id": "3",
            "Data": 2
          }
        ]
      }
    },
    {
      "$ref": "2"
    },
    {
      "$ref": "3"
    }
  ]
}
请注意,JSON深度现在受到严格限制。例如

请注意,为类型指定自定义转换器后,它需要手动执行所有操作。如果您的类型
Chacha
是多态的,并且您需要读取和写入
“$type”
属性,那么您需要自己将该逻辑添加到转换器中

顺便说一下,我建议使用
typenameholling.Objects
而不是
typenameholling.All
。可以在JSON中合理地指定对象类型(只要类型正确),但应该在代码中指定集合类型。这样做可以从数组切换到
列表
,而无需后期读取遗留JSON文件。

公共类模型
public class Model
 {
    public int Id { get; set; }
    public string Name { get; set; }
    public string SurName { get; set; }
 }
List<Model> list = new List<Model>();
list.Add(new Model { Id = 1, Name = "Jon", SurName = "Snow"});
var stringJson = JsonConvert.SerializeObject(list, new JsonSerializerSettings
  {
    PreserveReferencesHandling = PreserveReferencesHandling.Objects
  });
{ 公共int Id{get;set;} 公共字符串名称{get;set;} 公共字符串姓氏{get;set;} } 列表=新列表(); 添加(新模型{Id=1,Name=“Jon”,Name=“Snow”}); var stringJson=JsonConvert.SerializeObject(列表,新JsonSerializerSettings { PreserveReferencesHandling=PreserveReferencesHandling.Objects });
问题是什么?“对象具有相互引用的成员”← 这是你的问题。它是一个树形结构还是可以包含循环和自引用?(您的实际代码,而不是示例)“无法尝试捕获(idk why)”
PreserveReferencesHandling.Objects
始终序列化每个对象的第一次出现,并且所有对象的第一次出现在
步骤的索引0处。也就是说,您试图将90000个项目的链表序列化为90000个层次的嵌套JSON对象。当然,您会遇到堆栈溢出异常。请编写代码,将列表转换为
IEnumerable
,然后将其序列化。无论链表有多大,都可以通过将其序列化为一个数组来实现这一点,而开销不超过常量。您是否可以让JSON.NET在没有自定义序列化的情况下实现这一点是另一回事。更好的是,只需使用
LinkedList
,而不是自己滚动。更好的方法是使用
列表
,因为链接列表由于其位置问题,在家庭作业之外很少是有效的解决方案。谢谢。基于您的想法,我们创建了自己的序列化程序
public class Model
 {
    public int Id { get; set; }
    public string Name { get; set; }
    public string SurName { get; set; }
 }
List<Model> list = new List<Model>();
list.Add(new Model { Id = 1, Name = "Jon", SurName = "Snow"});
var stringJson = JsonConvert.SerializeObject(list, new JsonSerializerSettings
  {
    PreserveReferencesHandling = PreserveReferencesHandling.Objects
  });