关于c#:JSON.Net在使用[JsonConvert()]时抛出StackOverflowException

JSON.Net throws StackOverflowException when using [JsonConvert()]

我将这个简单的代码编写为Serialize类为flatten,但是当我使用[JsonConverter(typeof(FJson))]注释时,它会抛出一个StackOverflowException。 如果我手动调用SerializeObject,它可以正常工作。

如何在注释模式下使用JsonConvert:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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
        }
    }

在阅读(并测试)Paul Kiar和p.kaneman解决方案后,我会说实施WriteJson似乎是一项具有挑战性的任务。即使它适用于大多数情况 - 有一些尚未涵盖的边缘情况。
例子:

  • public bool ShouldSerialize*()方法
  • null
  • 值类型(struct)
  • json转换器属性
  • ..

这是(只是)另一个尝试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
    if (ReferenceEquals(value, null)) {
        writer.WriteNull();
        return;
    }

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

    writer.WriteStartObject();

    foreach (var property in contract.Properties) {
        if (property.Ignored) continue;
        if (!ShouldSerialize(property, value)) continue;

        var property_name = property.PropertyName;
        var property_value = property.ValueProvider.GetValue(value);

        writer.WritePropertyName(property_name);
        if (property.Converter != null && property.Converter.CanWrite) {
            property.Converter.WriteJson(writer, property_value, serializer);
        } else {
            serializer.Serialize(writer, property_value);
        }
    }

    writer.WriteEndObject();
}

private static bool ShouldSerialize(JsonProperty property, object instance) {
    return property.ShouldSerialize == null
        || property.ShouldSerialize(instance);
}


Json.NET没有方便的支持转换器调用JToken.FromObject生成"默认"序列化然后修改结果JToken输出 - 正是因为StackOverflowException由于递归调用JsonConverter.WriteJson()而导致观察会发生。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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

1
2
3
4
[JsonConverter(typeof(FJson))]
public class A
{
}

演示小提琴#1在这里。

第二种更简单的解决方法是为应用了JsonConverter的类型生成默认序列化,这有利于应用于成员的转换器取代应用于该类型或设置的转换器。来自文档:

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中,其中一个成员的值是您的类型的实例,并且应用了一个虚拟转换器,它只会回退到默认序列化以进行读取和写作。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public static 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 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;
    }

    [JsonObject(NamingStrategyType = typeof(DefaultNamingStrategy), IsReference = false)]
    class DefaultSerializationDTO< T >
    {
        public DefaultSerializationDTO() { }

        public DefaultSerializationDTO(T value) { this.Value = value; }

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

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()中使用它如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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();
}

这种方法的优点是:

  • 它不依赖递归禁用转换器,因此可以正确处理递归数据模型。

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

  • 演示小提琴#2在这里。

    笔记

    • 两种转换器版本只处理写入;阅读没有实现。

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

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

      1
      2
      3
      4
      5
      {
       "id": 1,
       "name": null,
       "name":"value"
      }

      这虽然不是严格违法,但通常被认为是不良做法,因此应该避免。


    我不喜欢上面发布的解决方案,所以我弄清楚序列化器如何实际序列化对象并尝试将其提取到最小值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer )
    {
       JsonObjectContract contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract( value.GetType() );

       writer.WriteStartObject();
       foreach ( var property in contract.Properties )
       {
          writer.WritePropertyName( property.PropertyName );
          writer.WriteValue( property.ValueProvider.GetValue(value));
       }
       writer.WriteEndObject();
    }

    没有堆栈溢出问题,也不需要递归禁用标志。


    我还不能发表评论,很抱歉......但我只想在Paul Kiar提供的解决方案中加入一些内容。他的解决方案真的帮助了我。

    保罗的代码简短,只需在没有任何自定义对象构建的情况下工作。
    我想要做的唯一补充是插入一个检查属性是否被忽略。如果将其设置为忽略,则跳过该属性的写入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            JsonObjectContract contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());

            writer.WriteStartObject();
            foreach (var property in contract.Properties)
            {
                if (property.Ignored)
                    continue;

                writer.WritePropertyName(property.PropertyName);
                writer.WriteValue(property.ValueProvider.GetValue(value));
            }
            writer.WriteEndObject();
        }

    通过将属性放在类A上,它将被递归调用。 WriteJson覆盖的第一行再次调用A类的序列化器。

    1
    JToken t = JToken.FromObject(value);

    这会导致递归调用,从而导致StackOverflowException。

    从您的代码中,我认为您正试图平息层次结构。您可以通过将转换器属性放在属性B上来实现此目的,这将避免递归。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //remove the converter from here
    public class A
    {
        public A()
        {
            this.b = new B();
        }

        public int id { get; set; }
        public string name { get; set; }
        [JsonConverter(typeof(FJson))]
        public B b { get; set; }
    }

    警告:你在这里得到的Json将有两个名为"name"的键,一个来自A类,另一个来自B类。