Here is a fun problem: how do you deserialize an array of objects with different types, but all of which inherit from the same super class?
If you are using Newtonsoft's Json.NET, then this is actually rather easy to implement!
Example
Here are three classes...
public abstract class Pet { public string Name { get; set; } }
public class Dog : Pet { public string FavoriteToy { get; set; } }
public class Cat : Pet { public bool WantsToKillYou { get; set; } }
...here is an array with instances of those objects mixed together...
new Pet[]
{
new Cat { Name = "Sql", WantsToKillYou = true },
new Cat { Name = "Linq", WantsToKillYou = false },
new Dog { Name = "Taboo", FavoriteToy = "Sql" }
}
...and now let's make it serialize and deseriailze! :)
Extending the JsonConverter
This tactic is actually quite simple! You need to extend a JsonConverter for your specific super class that is able to somehow uniquely identify each child class. In this example we look for a specific property that only exists on the child class, and Newtonsoft's JObjects and JTokens make this very easy to do!
public abstract class AbstractJsonConverter<T> : JsonConverter
{
protected abstract T Create(Type objectType, JObject jObject);
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override object ReadJson(
JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
{
var jObject = JObject.Load(reader);
T target = Create(objectType, jObject);
serializer.Populate(jObject.CreateReader(), target);
return target;
}
public override void WriteJson(
JsonWriter writer,
object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
protected static bool FieldExists(
JObject jObject,
string name,
JTokenType type)
{
JToken token;
return jObject.TryGetValue(name, out token) && token.Type == type;
}
}
public class PetConverter : AbstractJsonConverter<Pet>
{
protected override Pet Create(Type objectType, JObject jObject)
{
if (FieldExists(jObject, "FavoriteToy", JTokenType.String))
return new Dog();
if (FieldExists(jObject, "WantsToKillYou", JTokenType.Boolean))
return new Cat();
throw new InvalidOperationException();
}
}
Unit Tests
Now let's test the PetConverter against the the example array from above and see whether or not it works as expected. (Spoiler Alert: It works just fine!)
public class AbstractJsonConverterTests
{
[Fact]
public void PetConverter()
{
var originalArray = new Pet[]
{
new Cat { Name = "Sql", WantsToKillYou = true },
new Cat { Name = "Linq", WantsToKillYou = false },
new Dog { Name = "Taboo", FavoriteToy = "Sql" }
};
var json = JsonConvert.SerializeObject(originalArray);
var converter = new PetConverter();
var deserializedArray = JsonConvert.DeserializeObject<Pet[]>(
json,
converter);
Assert.Equal(originalArray.Length, deserializedArray.Length);
for (var i = 0; i < originalArray.Length; i++)
{
var original = originalArray[i];
var deserialized = deserializedArray[i];
Assert.Equal(original.GetType(), deserialized.GetType());
Assert.Equal(original.Name, deserialized.Name);
}
}
}
Enjoy,
Tom
Hate to break it to you - but there is a much easier way, just use these settings:
ReplyDeletevar settings = new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.Auto};
No converter or anything, it just works.
- Poul
Excellent Poul, thanks to disclose it
DeleteYes, that can work and it is easy, but it couples you to type/namespace specific information about the objects. While the option in this article requires a bit more code on the part of the deserializer, it offers a much more flexible contract for the serializer.
DeleteI know this is old, but using typenamehandling auto is TERRIBLE.
Deleteit opens a whole slew of security issue because you can tell newtonsoft to instantiate classes.. that can run processes on the machine.
It is not working when you have no control of serialization
DeleteHow can I use this when my abstract class is used in a property of a property of another class?
ReplyDelete