38

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 Answer 1

64

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(); 
    }
}
Sign up to request clarification or add additional context in comments.

5 Comments

And/or you could load your JSON as a JToken and check for token.Type == JTokenType.Null. Though actually your solution seems best.
In my case, the reader didn't has the correct state when returning null. Therefore, I came up with a combination of the answer's solution and the suggestion of [dbc]: if (reader.TokenType == JsonToken.Null) { JToken.Load(reader); return null; }.
Hi Jeroen, just writing to say your suggestion helped fix my problem, though to be totally honest, I have no idea why. My initial code had something similar to yours, where in the ReadJson I tried simply saying "if (reader.Value is Animal)", however, reader.Value was always null (despite it actually having a value). In the end I just went with your idea, and used "jsonObject[MyValue] != null" as you did with Lives/StopPhrase.
@KiranRamaswamy Glad to hear my Q&A was helpful to you!
@KiranRamaswamy I had the same confusion as you, although now I understand that reader.Value is null despite it actually having a value because the initial token is a JsonToken.StartObject, e.g. literally a { (which doesn't have a value)!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.