0

I've made a custom Json.NET JsonConverter which contains code like the following:

public class MyConverter : JsonConverter 
{
  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    .
    .
    .
    serializer.Serialize(writer, value);
    .
    .
    .
  }
}

[JsonConverter(typeof(MyConverter))]
public class MyJsonClass { 
  .
  .
  .
  public MyContainer<MyJsonClass> x { get; set; }
  .
  .
  .
}

I would like the call the .Serialize method ignoring the JsonConverter when inside the custom MyConverter. Basically doing my custom business then falling back to the default converter. Note I can't use the trick here of having a thread static variable and setting under recursion (at least in the way I understand), because this JsonConverter can and should be able to recurse into itself, namely when serialising MyContainer<MyJsonClass>, which may contain instances of MyJsonClass which should be serialised by first calling the MyConverter custom converter, before then calling the default converter again.

dbc
  • 104,963
  • 20
  • 228
  • 340
Clinton
  • 22,361
  • 15
  • 67
  • 163
  • Some of the answers from [JSON.Net throws StackOverflowException when using `[JsonConvert()]`](https://stackoverflow.com/q/29719509/3744182) may work for you - but there are no easy options when your classes are recursive. You could use the ***second, simpler workaround*** from [this answer](https://stackoverflow.com/a/29720068/3744182) -- but this involves serializing to a temporary `JObject` hierarchy and then editing it, which may not be performant enough for you if your recursion level is deep. – dbc May 26 '21 at 18:19
  • Or you could use Json.NET's contract resolver to get the serializable properties of `MyConverter` and serialize them all yourself as shown in [this answer](https://stackoverflow.com/a/42533701/3744182) – dbc May 26 '21 at 18:20
  • I don't suppose you could provide a [mcve] could you? – dbc May 26 '21 at 18:51
  • Are you sure you can't do what you need using [serialization callbacks](https://www.newtonsoft.com/json/help/html/SerializationCallbacks.htm)? – dbc May 26 '21 at 19:53

1 Answers1

1

What you want to do is to generate a default serialization for an object from within a JsonConverter.WriteJson() method when the data model being serialized is recursive so nested objects of the same type will be encountered, and need to be serialized using the converter. You have correctly concluded that disabling the converter using a thread static Boolean variable as shown in this answer to JSON.Net throws StackOverflowException when using [JsonConvert()] will not serialize these child objects correctly.

So, what are your options in such a situation?

Firstly, you could convert the thread static bool to a Stack<bool>, and push a flag to re-enable the converter inside [OnSerializing] and [OnDeserializing] methods within the object itself. The flag would then be popped in the [OnSerialized] and [OnDeserialized] methods. The details are as follows.

Say your data model looks like this:

public partial class MyJsonClass 
{
    public MyJsonClass x { get; set; }
}

And for some reason you want to serialize each instance of MyJsonClass inside some wrapper object like so:

{ "MyJsonClass":{ /* The contents of the MyJsonClass instance */ }

Then you can modify MyJsonClass as follows, using the following JsonConverter that supports pushing and popping of a Disabled status.

[JsonConverter(typeof(MyConverter))]
public partial class MyJsonClass 
{
    [OnSerializing]
    void OnSerializingMethod(StreamingContext context) => MyConverter.PushDisabled(false); // Re-enable the converter after we begin to serialize the object

    [OnSerialized]
    void OnSerializedMethod(StreamingContext context) => MyConverter.PopDisabled(); // Restore the converter to its previous state when serialization is complete

    [OnDeserializing] 
    void OnDeserializingMethod(StreamingContext context) => MyConverter.PushDisabled(false); // Re-enable the converter after we begin to deserialize the object

    [OnDeserialized]
    void OnDeserializedMethod(StreamingContext context) => MyConverter.PopDisabled(); // Restore the converter to its previous state when deserialization is complete
}

sealed class MyConverter : RecursiveConverterBase<MyJsonClass, MyConverter>
{
    class DTO { public MyJsonClass MyJsonClass { get; set; } }

    protected override void WriteJsonWithDefault(JsonWriter writer, object value, JsonSerializer serializer)
    {
        // Add your custom logic here.
        writer.WriteStartObject();
        writer.WritePropertyName(nameof(DTO.MyJsonClass));
        serializer.Serialize(writer, value);
        writer.WriteEndObject();
    }

    protected override object ReadJsonWithDefault(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        using (var pushValue = PushDisabledUsing(true))
        {
            // Add your custom logic here.
            return serializer.Deserialize<DTO>(reader)?.MyJsonClass;
        }
    }
}

public abstract class RecursiveConverterBase<TValue, TConverter> : JsonConverter where TConverter : RecursiveConverterBase<TValue, TConverter>
{
    static readonly ThreadLocal<Stack<bool>> disabledStack = new (() => new Stack<bool>());

    public static StackExtensions.PushValue<bool> PushDisabledUsing(bool disable) => disabledStack.Value.PushUsing(disable);
    public static void PushDisabled(bool disable)
    {
        if (InSerialization)
            disabledStack.Value.Push(disable);
    }
    public static void PopDisabled()
    {
        if (InSerialization)
            disabledStack.Value.Pop();
    }
    static bool Disabled => disabledStack.IsValueCreated && disabledStack.Value.TryPeek(out var disabled) && disabled;
    static bool InSerialization => disabledStack.IsValueCreated && disabledStack.Value.Count > 0;

    public override bool CanRead => !Disabled;
    public override bool CanWrite => !Disabled;

    public override bool CanConvert(Type objectType) => typeof(TValue).IsAssignableFrom(objectType); // Or typeof(TValue) == objectType if you prefer.

    protected abstract void WriteJsonWithDefault(JsonWriter writer, object value, JsonSerializer serializer);

    public sealed override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        using (var pushValue = PushDisabledUsing(true))
        {
            WriteJsonWithDefault(writer, value, serializer);
        }
    }

    protected abstract object ReadJsonWithDefault(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer);

    public sealed override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        using (var pushValue = PushDisabledUsing(true))
        {
            return ReadJsonWithDefault(reader, objectType, existingValue, serializer);
        }
    }
}

public static class StackExtensions
{
    public class PushValue<T> : IDisposable
    {
        readonly Stack<T> stack;
        readonly int count;

        public PushValue(T value, Stack<T> stack)
        {
            this.stack = stack;
            this.count = stack.Count;
            stack.Push(value);
        }

        // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
        public void Dispose()
        {
            if (stack != null)
            {
                while (stack.Count > count)
                    stack.Pop();
            }
        }
    }

    public static PushValue<T> PushUsing<T>(this Stack<T> stack, T value)
    {
        if (stack == null)
            throw new ArgumentNullException();
        return new PushValue<T>(value, stack);
    }
}

And now if you have an instance of MyJsonClass like so:

var myClass = new MyJsonClass
{
    x = new MyJsonClass
    {
        x = new MyJsonClass { }
    },
};

It will be serialized as follows:

{
  "MyJsonClass": {
    "x": {
      "MyJsonClass": {
        "x": {
          "MyJsonClass": {
            "x": null
          }
        }
      }
    }
  }
}

Notes:

  • Even though the converter state is pushed in On[De]Serializing and popped in On[De]Serialized, there is a chance that On[De]Serialized will not get called if an exception is thrown during serialization, for instance some sort of IOException.

    If that were to happen, the disabledStack would not get correctly restored, and future serializations might get corrupted.

    To prevent that, in JsonConverter.ReadJson() and WriteJson(), rather than simply popping the disabled state, we remember the initial stack depth and restore the stack to that depth. And in the serialization callbacks, the push and pop calls do nothing unless actually inside a JsonConverter method.

Demo fiddle here.

Secondly, if you are writing to an intermediate JToken hierarchy rather than directly to the incoming JsonWriter writer, you could use the second, simpler workaround JToken JsonExtensions.DefaultFromObject(this JsonSerializer serializer, object value) from this answer to JSON.Net throws StackOverflowException when using [JsonConvert()].

However, your question indicates that you are writing directly to the incoming writer, so this may not apply.

Thirdly, you could use the serializer's contract resolver to get the JsonObjectContract for your type, then cycle through the properties and serialize each one manually as per its contract information. For details, see this answer by Daniel Müller to JSON.Net throws StackOverflowException when using [JsonConvert()].

dbc
  • 104,963
  • 20
  • 228
  • 340