6

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.

Community
  • 1
  • 1
Jeroen
  • 60,696
  • 40
  • 206
  • 339

2 Answers2

1

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;
    }
}
Amit Kumar Ghosh
  • 3,618
  • 1
  • 20
  • 24
  • Upvoted as this led to a [working answer](http://stackoverflow.com/a/35310669/419956). Can't accept it as is though, as it will not pass the test / it still has a bug or two. – Jeroen Feb 10 '16 at 09:02
0

Based off @AmitKumarGhosh's suggestion, if you want to add the responsibility for lists to the same converter, you could do this to make the tests pass:

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)
        {
            return JArray.Load(reader)
                .Cast<JObject>()
                .Select(o => JObjectToMammal(o, serializer))
                .ToList();
        }

        return JObjectToMammal(JObject.Load(reader), serializer);
    }

    private Mammal JObjectToMammal(JObject jsonObject, JsonSerializer serializer)
    {
        if (jsonObject["Lives"] != null) return jsonObject.ToObject<Cat>(serializer);
        if (jsonObject["Drools"] != null) return jsonObject.ToObject<Dog>(serializer);
        return null;
    }
}
Jeroen
  • 60,696
  • 40
  • 206
  • 339
  • Your code looks promising but is throwing a Stackoverflow Exception... It seems that jsonObject.ToObject is calling ReadJson – Bidou May 31 '16 at 12:41
  • @Bidou Hmm, weird. As far as I can remember I had used the code from the question *as is* with this solution, and the unit tests worked fine. Is your repro also *exactly* the same, or subtly different? What version NewtonSoft did you use? – Jeroen May 31 '16 at 12:51
  • Mmhmhm I managed to make it work by creating a new class that derives from `DefaultContractResolver`. This prevent this weird behavior... – Bidou May 31 '16 at 13:12