Rasool Ghafari Rasool Ghafari - 4 months ago 80
JSON Question

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

dbc dbc
Answer

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 a StackOverflowException will occur.

One workaround is to temporarily disable the converter in recursive calls:

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

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
}

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

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

Update

In some situations, for instance , instances of JSON converters will be shared between threads. In that case, disabling the converter via an instance property will not be thread-safe. Instead, a [ThreadStatic] can be used to disable the converter:

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

    // And the remainder is as in the original answer:

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

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