Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
        }
    }
like image 700
Rasool Ghafari Avatar asked Apr 18 '15 15:04

Rasool Ghafari


2 Answers

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);
}
like image 72
Daniel Müller Avatar answered Oct 07 '22 08:10

Daniel Müller


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.

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
}

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

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

Demo fiddle #1 here.

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:

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.

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

And then use it in FJson.WriteJson() as follows:

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. It doesn't rely on recursively disabling the converter, and so works correctly with recursive data models.

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

  3. 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.

Demo fiddle #2 here.

Notes

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

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

  • 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.

like image 34
dbc Avatar answered Oct 07 '22 09:10

dbc