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

c#, json, json.net, serialization

Solution

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:

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.

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.

Problem

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 } } ```

Original source

Related problems