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);
}
}