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

10,373

Solution 1

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

Solution 2

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

Solution 3

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.

Solution 4

I can't comment yet, so sorry for that...but I just wanted to add something to the solution provided by Paul Kiar. His solution really helped me out.

The code of Paul is short and simply works without any custom building of objects. The only addition I would like to make is to insert a check if the property is ignored. If it is set to be ignored then skip the write for that property:

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

Solution 5

By placing the attribute on class A, it is being called recursively. The first line in WriteJson override is again calling the serializer on class A.

JToken t = JToken.FromObject(value);

This causes a recursive call and hence the StackOverflowException.

From your code, I think you are trying to flatten the heirarchy. You can probably achieve this by putting the converter attribute on the property B, which will avoid the recursion.

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

Warning: The Json you get here will have two keys called "name" one from class A and the other from class B.

Share:
10,373

Related videos on Youtube

Rasool Ghafari
Author by

Rasool Ghafari

Updated on July 18, 2022

Comments

  • Rasool Ghafari
    Rasool Ghafari over 1 year

    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
            }
        }
    
  • Les
    Les almost 7 years
    +1 An answer not accepted and with 0 upvotes doesn't normally get attention but I'd like to point out that this is the most evolved solution that got me closer to where I wanted to be (I also needed ignore null values when serializing certain properties but that was easy after having this). Thanks