1

The details of the problem might be a little long, so I'll describe it in short at the beginning: How to force Json.Net to use its default object serializer(or ignore a specific custom converter in other words), but still keep the settings in a JsonSerializer, when deserializing an object?
Apologize for my poor English, the description may be kind of ambiguous and confusing. I'll explain it with my detailed scenario.
When dealing with HTTP responses, we sometimes encounter a scenario that an object is the only child of its parent, making the parent object a meaningless object wrapper to some extent. In some poor designs, there could be multiple levels of such wrappers. If we want such JSON deserialized properly without customizing, we have to follow the structure to define those wrapper classes, which is definitely pointless and annoying, thus I came up with the idea to create a general-purpose ObjectWrapperConverter. Here's the code:

public class ObjectWrapperConverter<T> : ObjectWrapperConverterBase<T> {
    public ObjectWrapperConverter(string propertyName) : this(propertyName, Array.Empty<JsonConverter>()) { }

    public ObjectWrapperConverter(string propertyName, params JsonConverter[] converters) {
        PropertyName = propertyName;
        Converters = converters;
    }

    public override string PropertyName { get; }

    public override JsonConverter[] Converters { get; }
}

public abstract class ObjectWrapperConverterBase<T> : JsonConverter<T> {
    public abstract string PropertyName { get; }

    public abstract JsonConverter[] Converters { get; }

    public sealed override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer) {
        writer.WriteStartObject();
        writer.WritePropertyName(PropertyName);
        serializer.Converters.AddRange(Converters);
        writer.WriteValue(value, serializer);
        serializer.Converters.RemoveRange(Converters);
        writer.WriteEndObject();
    }

    public sealed override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, JsonSerializer serializer) {
        var token = JToken.Load(reader);
        if (token.Type != JTokenType.Object)
            throw new JTokenTypeException(token, JTokenType.Object);
        var obj = token as JObject;
        var prop = obj!.Property(PropertyName);
        if (prop is null)
            throw new JTokenException(token, $"Property \"{PropertyName}\" not found");
        serializer.Converters.AddRange(Converters);
        var result = prop.Value.ToObject<T>(serializer);//BUG: recurse when applying JsonConverterAttribute to a class
        serializer.Converters.RemoveRange(Converters);
        return result;
    }
}

It works fine when I put JsonConverterAttribute on properties and fields. But when annotating class, problem occurs: the deserialization process fall into a recursive loop.
I debugged into Json.Net framework, and realized that when specifying a custom converter for a class, Json.Net will always use this converter to handle the serialization of this class unless higher-priority attribute (like JsonConverterAttribute placed on properties) is annotated. Thus, in my converter, the line where I put a comment will finally lead to a recurse. If you've understood the purpose of this converter, it's easy to find out that this converter is just a middleware: add or remove the wrapper object, and continue the original serialization process.
So, how can I continue the "original" serialization process instead of falling into the converter itself again?

0x269
  • 688
  • 8
  • 20
  • 1
    For some options see [JSON.Net throws StackOverflowException when using `[JsonConvert()]`](https://stackoverflow.com/q/29719509/3744182). In fact this may be a duplicate, agree? Not 100% sure because your question lacks a full [mcve]. – dbc Aug 04 '21 at 14:58
  • @dbc Thanks for your link! It's exactly what I'm looking for. I've managed to solve this problem in a tricky way. I'll answer the question myself in case someone encounters similar problems. – 0x269 Aug 05 '21 at 15:07

1 Answers1

0

I made a deeper exploration into the Newtonsoft.Json framework and found out how it serialize and deserialize objects without converters.
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal
Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue
Because these classes and methods are internal, I have to use Reflection to invoke them. Thus I encapsulate this into two extension methods:

public static class NewtonsoftExtensions{
    private static Type JsonSerializerInternalReader { get; } = typeof(JsonSerializer).Assembly.GetType("Newtonsoft.Json.Serialization.JsonSerializerInternalReader");

    private static Type JsonSerializerInternalWriter { get; } = typeof(JsonSerializer).Assembly.GetType("Newtonsoft.Json.Serialization.JsonSerializerInternalWriter");

    private static MethodInfo CreateValueInternal { get; } = JsonSerializerInternalReader.GetMethod("CreateValueInternal", BindingFlags.NonPublic | BindingFlags.Instance);

    private static MethodInfo SerializeValue { get; } = JsonSerializerInternalWriter.GetMethod("SerializeValue", BindingFlags.NonPublic | BindingFlags.Instance);

    public object DeserializeWithoutContractConverter(this JsonSerializer serializer, JsonReader reader, Type objectType) {
        var contract = serializer.ContractResolver.ResolveContract(objectType);
        var converter = contract.Converter;
        contract.Converter = null;
        object internalReader = Activator.CreateInstance(JsonSerializerInternalReader, serializer);
        object result = CreateValueInternal.Invoke(internalReader, reader, objectType, contract, null, null, null, null);
        contract.Converter = converter; //DefaultContractResolver caches the contract of each type, thus we need to restore the original converter for future use
        return result;
    }

    public void SerializeWithoutContractConverter(this JsonSerializer serializer, JsonWriter writer, object value) {
        var contract = serializer.ContractResolver.ResolveContract(value.GetType());
        var converter = contract.Converter;
        contract.Converter = null;
        object internalWriter = Activator.CreateInstance(JsonSerializerInternalWriter, serializer);
        SerializeValue.Invoke(internalWriter, writer, value, contract, null, null, null);
        contract.Converter = converter;
    }
}

Using reflection to call internal methods is risky and should not be recommended, but compared with other answers in JSON.Net throws StackOverflowException when using [JsonConvert()], such approach would make full use of serializer settings. If the converter is general-purpose, like the ObjectWrapperConverter I'm trying to implement, this will cause least the unexpected results, as Newtonsoft.Json has tons of settings for users to customize the behaviors.

0x269
  • 688
  • 8
  • 20