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

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.

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 does not have convenient support for converters that call JToken.FromObject to generate a "default" serialization, then modify the resulting JToken for output - precisely because the StackOverflowException that you have observed will occur.

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 that case, 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
{
    Func<T> getValue;
    Action<T> setValue;
    T oldValue;

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

Note this converter only does writing; reading is not implemented.

Incidentally, 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

If you are sure your converter will not be shared across threads, you can use a member variable instead:

public class FJson : JsonConverter
{
    bool CannotWrite { get; set; }

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

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JToken t;
        using (new PushValue<bool>(true, () => CannotWrite, (canWrite) => CannotWrite = 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
    }
}

In this scheme, it is necessary to call JToken.FromObject(Object, JsonSerializer) and pass down the incoming serializer, so that the same instance of your converter FJson is used. Having done this, you can restore the [JsonConverter(typeof(FJson))] to your class A :

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

I didn't like the solution posted above so I worked out how the serializer actually serialized the object and tried to distill it down to the minimum:

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

No stack overflow problem and no need for a recursive disable flag.


After reading (and testing) Paul Kiar & p.kaneman solution I'd say it seems to be a challenging task to implement WriteJson . Even though it works for the most cases - there are a few edge cases that are not covered yet. Examples:

  • public bool ShouldSerialize*() methods
  • null values
  • value types ( struct )
  • json converter attributes
  • ..
  • Here is (just) another try:

    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);
    }
    
    链接地址: http://www.djcxy.com/p/37832.html

    上一篇: 需要使用JSON为REST v2 API传递Jasper报表参数的示例

    下一篇: JSON.Net在使用[JsonConvert()]时抛出StackOverflowException