JSON.Net 在使用 [JsonConvert()] 时抛出 StackOverflowException [英] JSON.Net throws StackOverflowException when using [JsonConvert()]

查看:21
本文介绍了JSON.Net 在使用 [JsonConvert()] 时抛出 StackOverflowException的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我编写了这个简单的代码来将类序列化为扁平化,但是当我使用 [JsonConverter(typeof(FJson))] 注释时,它会抛出一个 StackOverflowException.如果我手动调用 SerializeObject,它工作正常.

I wrote this simple code to Serialize classes as flatten, but when I use [JsonConverter(typeof(FJson))] annotation, it throws a StackOverflowException. If I call the SerializeObject manually, it works fine.

如何在 Annotation 模式下使用 JsonConvert:

How can I use JsonConvert in Annotation mode:

class Program
    {
        static void Main(string[] args)
        {
            A a = new A();
            a.id = 1;
            a.b.name = "value";

            string json = null;

            // json = JsonConvert.SerializeObject(a, new FJson()); without [JsonConverter(typeof(FJson))] annotation workd fine
            // json = JsonConvert.SerializeObject(a); StackOverflowException

            Console.WriteLine(json);
            Console.ReadLine();
        }
    }

    //[JsonConverter(typeof(FJson))] StackOverflowException
    public class A
    {
        public A()
        {
            this.b = new B();
        }

        public int id { get; set; }
        public string name { get; set; }
        public B b { get; set; }
    }

    public class B
    {
        public string name { get; set; }
    }

    public class FJson : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            JToken t = JToken.FromObject(value);
            if (t.Type != JTokenType.Object)
            {
                t.WriteTo(writer);
                return;
            }

            JObject o = (JObject)t;
            writer.WriteStartObject();
            WriteJson(writer, o);
            writer.WriteEndObject();
        }

        private void WriteJson(JsonWriter writer, JObject value)
        {
            foreach (var p in value.Properties())
            {
                if (p.Value is JObject)
                    WriteJson(writer, (JObject)p.Value);
                else
                    p.WriteTo(writer);
            }
        }

        public override object ReadJson(JsonReader reader, Type objectType,
           object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }

        public override bool CanConvert(Type objectType)
        {
            return true; // works for any type
        }
    }

推荐答案

Json.NET 对调用 JToken.FromObject 以生成默认"序列化,然后修改结果 JToken 用于输出 - 正是因为 StackOverflowException 由于对 JsonConverter.WriteJson() 的递归调用而导致您观察到的发生.

Json.NET does not have convenient support for converters that call JToken.FromObject to generate a "default" serialization and then modify the resulting JToken for output - precisely because the StackOverflowException due to recursive calls to JsonConverter.WriteJson() that you have observed will occur.

一种解决方法是使用线程静态布尔值在递归调用中临时禁用转换器.使用线程静态是因为,在某些情况下,包括 ,JSON 转换器的实例将在线程之间共享.在这种情况下,通过实例属性禁用转换器将不是线程安全的.

One workaround is to temporarily disable the converter in recursive calls using a thread static Boolean. A thread static is used because, in some situations including asp.net-web-api, instances of JSON converters will be shared between threads. In such situations disabling the converter via an instance property will not be thread-safe.

public class FJson : JsonConverter
{
    [ThreadStatic]
    static bool disabled;

    // Disables the converter in a thread-safe manner.
    bool Disabled { get { return disabled; } set { disabled = value; } }

    public override bool CanWrite { get { return !Disabled; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JToken t;
        using (new PushValue<bool>(true, () => Disabled, (canWrite) => Disabled = canWrite))
        {
            t = JToken.FromObject(value, serializer);
        }

        if (t.Type != JTokenType.Object)
        {
            t.WriteTo(writer);
            return;
        }

        JObject o = (JObject)t;
        writer.WriteStartObject();
        WriteJson(writer, o);
        writer.WriteEndObject();
    }

    private void WriteJson(JsonWriter writer, JObject value)
    {
        foreach (var p in value.Properties())
        {
            if (p.Value is JObject)
                WriteJson(writer, (JObject)p.Value);
            else
                p.WriteTo(writer);
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType,
       object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override bool CanConvert(Type objectType)
    {
        return true; // works for any type
    }
}

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
}

完成此操作后,您可以将 [JsonConverter(typeof(FJson))] 恢复到您的类 A:

Having done this, you can restore the [JsonConverter(typeof(FJson))] to your class A:

[JsonConverter(typeof(FJson))]
public class A
{
}

演示小提琴 #1 此处.

第二种更简单的解决方法,用于为应用了 JsonConverter 的类型生成默认的 JToken 表示,利用了转换器应用于成员取代应用于类型或设置中的转换器.来自 文档:

A second, simpler workaround for generating a default JToken representation for a type with a JsonConverter applied takes advantage fact that a converter applied to a member supersedes converters applied to the type, or in settings. From the docs:

使用 JsonConverter 的优先级是成员上的属性定义的 JsonConverter,然后是类上的属性定义的 JsonConverter,最后是传递给 JsonSerializer 的所有转换器.

The priority of which JsonConverter is used is the JsonConverter defined by attribute on a member, then the JsonConverter defined by an attribute on a class, and finally any converters passed to the JsonSerializer.

因此,可以通过将其嵌套在 DTO 有一个成员,其值是您的类型的一个实例,并且应用了一个虚拟转换器,它只会回退到读取和写入的默认序列化.

Thus it is possible to generate a default serialization for your type by nesting it inside a DTO with a single member whose value is an instance of your type and has a dummy converter applied which does nothing but fall back to to default serialization for both reading and writing.

以下扩展方法和转换器可以完成这项工作:

The following extension method and converter do the job:

public static partial class JsonExtensions
{
    public static JToken DefaultFromObject(this JsonSerializer serializer, object value)
    {
        if (value == null)
            return JValue.CreateNull();
        var dto = Activator.CreateInstance(typeof(DefaultSerializationDTO<>).MakeGenericType(value.GetType()), value);
        var root = JObject.FromObject(dto, serializer);
        return root["Value"].RemoveFromLowestPossibleParent() ?? JValue.CreateNull();
    }

    public static object DefaultToObject(this JToken token, Type type, JsonSerializer serializer = null)
    {
        var oldParent = token.Parent;
    
        var dtoToken = new JObject(new JProperty("Value", token));
        var dtoType = typeof(DefaultSerializationDTO<>).MakeGenericType(type);
        var dto = (IHasValue)(serializer ?? JsonSerializer.CreateDefault()).Deserialize(dtoToken.CreateReader(), dtoType);
        
        if (oldParent == null)
            token.RemoveFromLowestPossibleParent();
            
        return dto == null ? null : dto.GetValue();
    }
    
    public static JToken RemoveFromLowestPossibleParent(this JToken node)
    {
        if (node == null)
            return null;
        // If the parent is a JProperty, remove that instead of the token itself.
        var contained = node.Parent is JProperty ? node.Parent : node;
        contained.Remove();
        // Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
        if (contained is JProperty)
            ((JProperty)node.Parent).Value = null;
        return node;
    }
    
    interface IHasValue
    {
        object GetValue();
    }

    [JsonObject(NamingStrategyType = typeof(DefaultNamingStrategy), IsReference = false)]
    class DefaultSerializationDTO<T> : IHasValue
    {
        public DefaultSerializationDTO(T value) { this.Value = value; }

        public DefaultSerializationDTO() { }
        
        [JsonConverter(typeof(NoConverter)), JsonProperty(ReferenceLoopHandling = ReferenceLoopHandling.Serialize)]
        public T Value { get; set; }
        
        object IHasValue.GetValue() { return Value; }
    }
}

public class NoConverter : JsonConverter
{
    // NoConverter taken from this answer https://stackoverflow.com/a/39739105/3744182
    // To https://stackoverflow.com/questions/39738714/selectively-use-default-json-converter
    // By https://stackoverflow.com/users/3744182/dbc
    public override bool CanConvert(Type objectType)  { throw new NotImplementedException(); /* This converter should only be applied via attributes */ }

    public override bool CanRead { get { return false; } }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); }
}

然后在FJson.WriteJson()中使用如下:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    JToken t = serializer.DefaultFromObject(value);

    // Remainder as before
    if (t.Type != JTokenType.Object)
    {
        t.WriteTo(writer);
        return;
    }

    JObject o = (JObject)t;
    writer.WriteStartObject();
    WriteJson(writer, o);
    writer.WriteEndObject();
}

这种方法的优点和缺点是:

The advantages and disadvantages of this approach are that:

  1. 它不依赖于递归禁用转换器,因此可以与递归数据模型一起正常工作.

  1. It doesn't rely on recursively disabling the converter, and so works correctly with recursive data models.

它不需要重新实现从对象的属性序列化对象的整个逻辑.

It doesn't require re-implementing the entire logic of serializing an object from its properties.

它序列化为中间的 JToken 表示并从中反序列化.当尝试将默认序列化直接传入和传出传入的 JsonReaderJsonWriter 时,不适合使用.

It serializes to and deserializes from an intermediate JToken representation. It is not appropriate for use when attempt to stream a default serialization directly to and from a the incoming JsonReader or JsonWriter.

演示小提琴 #2 此处.

注意事项

  • 两个转换器版本都只处理写入;未实现读取.

  • Both converter versions only handle writing; reading is not implemented.

要解决序列化过程中的等效问题,请参见例如使用 JsonConverter 进行 Json.NET 自定义序列化 - 如何获得默认"行为.

To solve the equivalent problem during deserialization, see e.g. Json.NET custom serialization with JsonConverter - how to get the "default" behavior.

您编写的转换器创建了具有重复名称的 JSON:

Your converter as written creates JSON with duplicated names:

{
  "id": 1,
  "name": null,
  "name": "value"
}

这虽然不是严格非法的,但通常被认为是不良做法,因此应该避免.>

This, while not strictly illegal, is generally considered to be bad practice and so should probably be avoided.

这篇关于JSON.Net 在使用 [JsonConvert()] 时抛出 StackOverflowException的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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