Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to deserialize a list of abstract items without passing converters to DeserializeObject?

I've got some inheritance going, requiring a custom JsonConverter for deserialization. I'm using a very straightforward approach for now where I determine the type based on the existence of certain properties.

Important note: in my actual code I cannot touch the DeserializeObject calls, i.e. I cannot add custom convertors there. I know this is therefor to some degree an XY-problem, and realize as such my answer might be that what I want is not possible. As far as I can tell this makes my question slightly different from this question.

Here's a repro of my situation:

abstract class Mammal { }
class Cat : Mammal { public int Lives { get; set; } }
class Dog : Mammal { public bool Drools { get; set; } }
class Person
{
    [JsonConverter(typeof(PetConverter))]
    public Mammal FavoritePet { get; set; }

    [JsonConverter(typeof(PetConverter))]
    public List<Mammal> OtherPets { get; set; } 
}

And this is the custom converter:

public class PetConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) { return objectType == typeof(Mammal); }
    public override bool CanWrite { get { return false; } }
    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)
    {
        if (reader.TokenType == JsonToken.Null) return null;
        JObject jsonObject = JObject.Load(reader);
        if (jsonObject["Lives"] != null) return jsonObject.ToObject<Cat>(serializer);
        if (jsonObject["Drools"] != null) return jsonObject.ToObject<Dog>(serializer);
        return null;
    }
}

This works fine for the FavoritePet, but not so much for the OtherPets because it's a list. Here's a way to reproduce my problem with NUnit tests:

[TestFixture]
class MyTests
{
    [Test]
    public void CanSerializeAndDeserializeSingleItem()
    {
        var person = new Person { FavoritePet = new Cat { Lives = 9 } };
        var json = JsonConvert.SerializeObject(person);
        var actual = JsonConvert.DeserializeObject<Person>(json);
        Assert.That(actual.FavoritePet, Is.InstanceOf<Cat>());
    }

    [Test]
    public void CanSerializeAndDeserializeList()
    {
        var person = new Person { OtherPets = new List<Mammal> { new Cat { Lives = 9 } } };
        var json = JsonConvert.SerializeObject(person);
        var actual = JsonConvert.DeserializeObject<Person>(json);
        Assert.That(actual.OtherPets.Single(), Is.InstanceOf<Cat>());
    }
}

The latter test is red because:

Newtonsoft.Json.JsonReaderException : Error reading JObject from JsonReader. Current JsonReader item is not an object: StartArray. Path 'OtherPets', line 1, position 33.

I've also tried without the custom converter on OtherPets, which results in:

Newtonsoft.Json.JsonSerializationException : Could not create an instance of type JsonConverterLists.Mammal. Type is an interface or abstract class and cannot be instantiated. Path 'OtherPets[0].Lives', line 1, position 42.

I understand what's going on, I even know that I could fix it with:

var actual = JsonConvert.DeserializeObject<Person>(json, new PetConverter());

But repeating the note from above: I can't change the DeserializeObject call as it's wrapped inside a function in a library I cannot currently change.

Is there a way to do the same with a attribute-based approach, e.g. is there a built-in converter for lists where each entry takes in a custom converter? Or do I have to roll my own, seperate converter for this too?


Footnote, how to reproduce:

  • Visual Studio 2013 => Fresh new .NET 4.5.1 Class Library
  • Install-Package Newtonsoft.Json -Version 7.0.1
  • Install-Package nunit -Version 2.6.4

You can just drop the above three code blocks in your fresh namespace and run the NUnit tests, seeing the second one fail.

like image 863
Jeroen Avatar asked Feb 10 '16 07:02

Jeroen


People also ask

Can we deserialize an abstract class?

The JSON contains a list of objects which is abstract and it's not possible to deserialize it.

What does JsonConvert DeserializeObject?

DeserializeObject<T>(String,JsonConverter[]) Deserializes the JSON to the specified . NET type using a collection of JsonConverter.


1 Answers

tweaked the converter class a little bit. hope it's good -

public class PetConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) { return objectType == typeof(Mammal); }
    public override bool CanWrite { get { return false; } }
    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)
    {
        if (reader.TokenType == JsonToken.Null) return null;

        if (reader.TokenType == JsonToken.StartArray)
        {
            var li = new List<Mammal>();
            var arr = JArray.Load(reader);
            foreach (JObject obj in arr)
            {
                if (obj["Drools"] != null)
                {
                    var k = obj.ToObject<Dog>(serializer);
                    li.Add(k);
                }
            }

            return li;
        }


        JObject jsonObject = JObject.Load(reader);
        if (jsonObject["Lives"] != null) return jsonObject.ToObject<Cat>(serializer);
        //if (jsonObject["Drools"] != null) return jsonObject.ToObject<Dog>(serializer);
        return null;
    }
}
like image 147
Amit Kumar Ghosh Avatar answered Oct 31 '22 01:10

Amit Kumar Ghosh