32
votes

I've got a Newtonsoft JSON.NET JsonConverter to help deserialize a property whose type is an abstract class. The gist of it looks like this:

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

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

        if (jsonObject["Lives"] != null) return jsonObject.ToObject<Cat>(serializer);
        if (jsonObject["StopPhrase"] != null) return jsonObject.ToObject<Parrot>(serializer);

        return null;
    }

    public override bool CanWrite { get { return false; } }

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

Here are the classes it handles:

public abstract class Animal 
{ }

public class Cat : Animal
{
    public int Lives { get; set; }
}

public class Parrot : Animal
{
    public string StopPhrase { get; set; }
}

public class Person
{
    [JsonConverter(typeof(PetConverter))]
    public Animal Pet { get; set; }
}

This works fine when deserializing a Person that has a non-null Pet. But if the Pet is null, then the ReadJson method breaks on the first line with this a JsonReaderException:

An exception of type 'Newtonsoft.Json.JsonReaderException' occurred in Newtonsoft.Json.dll but was not handled in user code

Additional information: Error reading JObject from JsonReader. Current JsonReader item is not an object: Null. Path 'Pet', line 1, position 11.

I've checked the Custom JsonConverter documentation, but it is merely about a writing converter. I've tried the following:

if (reader.Value == null) return null; // this inverts the [Test] results

But then I get:

JsonSerializationException: Additional text found in JSON string after finishing deserializing object.

For cases when the property is populated.

In short, what is the proper way to handle this situation?


For completeness, here are some unit tests that demonstrate the issue at hand:

[TestFixture]
public class JsonConverterTests
{
    [Test]
    public void Cat_survives_serialization_roundtrip()
    {
        var person = new Person { Pet = new Cat { Lives = 9 } };
        var serialized = JsonConvert.SerializeObject(person);
        var deserialized = JsonConvert.DeserializeObject<Person>(serialized);
        Assert.That(deserialized.Pet, Is.InstanceOf<Cat>());
        Assert.That((deserialized.Pet as Cat).Lives, Is.EqualTo(9));
    }

    [Test]
    public void Parrot_survives_serialization_roundtrip()
    {
        var person = new Person { Pet = new Parrot { StopPhrase = "Lorrie!" } };
        var serialized = JsonConvert.SerializeObject(person);
        var deserialized = JsonConvert.DeserializeObject<Person>(serialized);
        Assert.That(deserialized.Pet, Is.InstanceOf<Parrot>());
        Assert.That((deserialized.Pet as Parrot).StopPhrase, Is.EqualTo("Lorrie!"));
    }

    [Test]
    public void Null_property_does_not_break_converter()
    {
        var person = new Person { Pet = null };
        var serialized = JsonConvert.SerializeObject(person);
        var deserialized = JsonConvert.DeserializeObject<Person>(serialized);
        Assert.That(deserialized.Pet, Is.Null);
    }
}
1

1 Answers

52
votes

While writing the question, specifically while writing the "what have I tried" bit, I find one possible solution:

if (reader.TokenType == JsonToken.Null) return null;

I'm posting this for two reasons:

  1. If it's good enough, it might help someone else with the same question.
  2. I might learn of a better, competing solution from someone else's answer.

FWIW, here's the full JsonConverter for very basic handling deserialization of a property whose type is an abstract class:

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

    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["StopPhrase"] != null) return jsonObject.ToObject<Parrot>(serializer);

        return null;
    }

    public override bool CanWrite { get { return false; } }

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