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()].