简体   繁体   中英

Custom inheritance JsonConverter fails when JsonConverterAttribute is used

I am trying to deserialize derived type, and I want to use a custom property Type to distinguish between derived types.

[
  {
    "Type": "a",
    "Height": 100
  },
  {
    "Type": "b",
    "Name": "Joe"
  }
]

The solution I came to was to create a custom JsonConverter . On ReadJson I read the Type property and instantiate that type through the ToObject<T> function. Everything works fine until I use a JsonConverterAttribute . The ReadJson method loops infinitely because the attribute is applied on subtypes too.

How do I prevent this attribute from being applied to the subtypes?

[JsonConverter(typeof(TypeSerializer))]
public abstract class Base
{
    private readonly string type;

    public Base(string type)
    {
        this.type = type;
    }

    public string Type { get { return type; } }
}

public class AType : Base
{
    private readonly int height;

    public AType(int height)
        : base("a")
    {
        this.height = height;
    }

    public int Height { get { return height; } }
}

public class BType : Base
{
    private readonly string name;

    public BType(string name)
        : base("b")
    {
        this.name = name;
    }

    public string Name { get { return name; } }
}

public class TypeSerializer : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Base);
    }

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var j = JObject.Load(reader);

        var type = j["Type"].ToObject<string>();

        if (type == "a")
            // Infinite Loop! StackOverflowException
            return j.ToObject<AType>(); 
        if (type == "b")
            return j.ToObject<BType>();

        throw new NotImplementedException(type);
    }
}

[TestFixture]
public class InheritanceSerializeTests
{
    [Test]
    public void Deserialize()
    {
        var json = @"{""Type"":""a"", ""Height"":100}";
        JObject.Parse(json).ToObject<Base>(); // Crash
    }
}

I had a very similar problem with a project that I am currently working on: I wanted to make a custom JsonConverter and map it to my entities via attributes, but then the code got trapped in an infinite loop.

What did the trick in my case was using serializer.Populate instead of JObject.ToObject (I couldn't use .ToObject even if I wanted to; I am using version 3.5.8, in which this function does not exist). Below is my ReadJson method as an example:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    JContainer lJContainer = default(JContainer);

    if (reader.TokenType == JsonToken.StartObject)
    {
        lJContainer = JObject.Load(reader);
        existingValue = Convert.ChangeType(existingValue, objectType);
        existingValue = Activator.CreateInstance(objectType);

        serializer.Populate(lJContainer.CreateReader(), existingValue);
    }

    return existingValue;
}

Remove the [JsonConverter(typeof(TypeSerializer))] attribute from the Base class and in the Deserialize test replace the following line:

JObject.Parse(json).ToObject<Base>(); // Crash

with this one:

var obj = JsonConvert.DeserializeObject<Base>(json, new TypeSerializer());

UPDATE 1 This update matches the comment from the asker of the question:

Leave the [JsonConverter(typeof(TypeSerializer))] attribute to the Base class. Use the following line for deserialization:

var obj = JsonConvert.DeserializeObject<Base>(json);

and modify the ReadJson method like this:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    var j = JObject.Load(reader);

    if (j["Type"].ToString() == "a")
        return new AType(int.Parse(j["Height"].ToString()));

    return new BType(j["Name"].ToString());
}

JsonConverters are inherited from base classes. There currently is no option to limit the JsonConverter to only a base class. You can overwrite it though.

Tested on Newtonsoft.Json 12.0.3

public class DisabledConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override bool CanConvert(Type objectType)
    {
        throw new NotImplementedException();
    }

    public override bool CanRead => false;
    public override bool CanWrite => false;
}

Then overwrite the JsonConverter on the derived classes.

[JsonConverter(typeof(DisabledConverter))]
public class AType : Base
...

[JsonConverter(typeof(DisabledConverter))]
public class BType : Base
...

Details

This only applies to the code:

if (type == "a")
    return j.ToObject<AType>(); 
if (type == "b")
    return j.ToObject<BType>();

When calling .ToObject it will try to use the converter (again) defined on the base class to deserialize to the object. This is what makes an infinite loop.

You need to override the JsonConverter on the derived class.

The CanRead => false and CanWrite => false will disable the custom JsonConverter for that class forcing the .ToObject call to use the default logic internally inside Newtonsoft.Json instead of your TypeSerializer class.

I had a similar problem to this and encountered the infinite loop.

The Api I was consuming could return an error response or the expected type. I got around the problem in the same way as others have highlighted with using serializer.Populate.

public class Error{
   public string error_code { get; set; }
   public string message { get; set; }
}

[JsonConverter(typeof(CustomConverter<Success>))]
public class Success{
   public Guid Id { get; set; }
}

public class CustomConverter<T> : JsonConverter where T : new() {
   public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
   {
      JObject jObject = JObject.Load(reader);
      if (jObject.ContainsKey("error_code")) {
          return jObject.ToObject(typeof(ProvisoErrorResponse));
      }

      var instance = new T();
      serializer.Populate(jObject.CreateReader(), instance);
      return instance;
   }
}

Then used by HttpClient like this:

using (var response = await _httpClient.GetAsync(url))
{
   return await response.Content.ReadAsAsync<Success>();
}

Why does this loop occur? I think we're assuming a fallacy. I initially tried calling base.ReadJson thinking that I was overriding existing functionality when in fact there are many JsonConverters and our custom converter isnt overriding anything as the base class has no real methods. It would be better to treat the base class like an interface instead. The loop occurs because the converter registered by us is the converter that the engine considers most applicable to the type to be converted. Unless we can remove our own converter from the converter list at runtime, calling on the engine to deserialize while within our custom converter will create infinite recursion.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM