序列化时JSON.NET StackOverflowException [英] JSON.NET StackOverflowException while serialization

查看:72
本文介绍了序列化时JSON.NET StackOverflowException的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

My C# program is running into StackOverflowException, when I try to serialize object with similar structure like this:

  • 对象具有互相引用的成员
  • 无法尝试捕获(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);
}

JSON.NET版本为:9.0.1
.NET Framework:4.5.2
任何解决方案如何序列化此结构?

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

JSON.NET version is: 9.0.1
.NET Framework: 4.5.2
Any solutions how to serialize this structure?

Any help or suggestion is welcomed. Thank you

推荐答案

得到stackoverflow异常的原因是,当

The reason you are getting the stackoverflow exception is that Json.NET is a recursive, single-pass tree or graph serializer that, when PreserveReferencesHandling.Objects is enabled, always serializes the first occurrence of each object. You have constructed your 15,000 element Chacha [] array so that the first entry is the head of a linked list containing all the other items linked sequentially. Json.NET will try to serialize that to nested JSON objects 15,000 levels deep via 15,000 levels of recursion, overflowing the stack in the process.

因此,您需要做的是将整个链接表仅写在列表的 head 处,作为一个JSON数组.但是,不幸的是,Json.NET还是基于契约的序列化器,这意味着无论嵌套深度是多少,只要遇到给定类型的对象,Json.NET都将尝试写入相同的属性.因此,将Chacha[] NextChachaList属性添加到Chacha对象没有帮助,因为它将在每个级别写入.相反,有必要创建一个相当复杂的自定义JsonConverter 以线程安全的方式跟踪序列化深度,并且仅在顶层写入链接列表.以下是技巧:

Thus what you need to do is write the entire table of linkages only at the head of the list, as a JSON array. Unfortunately, however, Json.NET is also a contract-based serializer which means it will try to write the same properties whenever it encounters an object of a given type, no matter what the nesting depth is. Thus adding a Chacha[] NextChachaList property to your Chacha object doesn't help since it will get written at each level. Instead it will be necessary to create a fairly complex custom JsonConverter that tracks the serialization depth in a thread-safe manner and only writes the linkage list only at the top level. The following does the trick:

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;
    }
}

然后,给出稍微修改的类Chacha:

Then, given the slightly modified class Chacha:

class Chacha
{
    public Chacha NextChacha { get; set; }

    public long Data { get; set; }
}

为3个项目的数组生成以下JSON:

The following JSON is generated for an array of 3 items:

{
  "$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深度现已严格限制.示例小提琴.

Notice that the JSON depth is now strictly limited. Example fiddle.

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

Be aware that, once you specify a custom converter for your type, it needs to do everything manually. If your type Chacha is polymorphic and you need to read and write "$type" properties, you will need to add that logic to the converter yourself.

顺便说一句,我建议使用TypeNameHandling.Objects而不是TypeNameHandling.All.可以在JSON中合理指定对象类型(只要类型已正确已清理)即可. ),但应在代码中指定集合类型.这样做可以从数组切换到List<T>,而不必重新读取旧版JSON文件.

By the way, I recommend TypeNameHandling.Objects instead of TypeNameHandling.All. Object types may reasonably be specified in the JSON (as long as the types are properly sanitized) but collection types should be specified in the code. Doing so makes it possible to switch from an array to a List<T> without having to postread legacy JSON files.

这篇关于序列化时JSON.NET StackOverflowException的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆