Discriminated Json Subtypes Converter implementation for .NET
JsonSubTypes is a discriminated Json sub-type Converter implementation for .NET
[JsonConverter(typeof(JsonSubtypes), "Kind")]
public interface IAnimal
{
string Kind { get; }
}
public class Dog : IAnimal
{
public string Kind { get; } = "Dog";
public string Breed { get; set; }
}
public class Cat : IAnimal {
public string Kind { get; } = "Cat";
public bool Declawed { get; set;}
}
The second parameter of the JsonConverter
attribute is the JSON property name that will be use to retreive the type information from JSON.
var animal = JsonConvert.DeserializeObject<IAnimal>("{\"Kind\":\"Dog\",\"Breed\":\"Jack Russell Terrier\"}");
Assert.AreEqual("Jack Russell Terrier", (animal as Dog)?.Breed);
N.B.: This only works for types in the same assembly as the base type/interface and either in the same namespace or with a fully qualified type name.
[JsonConverter(typeof(JsonSubtypes), "Sound")]
[JsonSubtypes.KnownSubType(typeof(Dog), "Bark")]
[JsonSubtypes.KnownSubType(typeof(Cat), "Meow")]
public class Animal
{
public virtual string Sound { get; }
public string Color { get; set; }
}
public class Dog : Animal
{
public override string Sound { get; } = "Bark";
public string Breed { get; set; }
}
public class Cat : Animal
{
public override string Sound { get; } = "Meow";
public bool Declawed { get; set; }
}
var animal = JsonConvert.DeserializeObject<IAnimal>("{\"Sound\":\"Bark\",\"Breed\":\"Jack Russell Terrier\"}");
Assert.AreEqual("Jack Russell Terrier", (animal as Dog)?.Breed);
N.B.: Also works with other kind of value than string, i.e.: enums, int, …
This mode of operation only works when JsonSubTypes is explicitely registered in JSON.NET’s serializer settings, and not through the [JsonConverter]
attribute.
public abstract class Animal
{
public int Age { get; set; }
}
public class Dog : Animal
{
public bool CanBark { get; set; } = true;
}
public class Cat : Animal
{
public int Lives { get; set; } = 7;
}
public enum AnimalType
{
Dog = 1,
Cat = 2
}
var settings = new JsonSerializerSettings();
settings.Converters.Add(JsonSubtypesConverterBuilder
.Of(typeof(Animal), "Type") // type property is only defined here
.RegisterSubtype(typeof(Cat), AnimalType.Cat)
.RegisterSubtype(typeof(Dog), AnimalType.Dog)
.SerializeDiscriminatorProperty() // ask to serialize the type property
.Build());
or using syntax with generics:
var settings = new JsonSerializerSettings();
settings.Converters.Add(JsonSubtypesConverterBuilder
.Of<Animal>("Type") // type property is only defined here
.RegisterSubtype<Cat>(AnimalType.Cat)
.RegisterSubtype<Dog>(AnimalType.Dog)
.SerializeDiscriminatorProperty() // ask to serialize the type property
.Build());
var cat = new Cat { Age = 11, Lives = 6 }
var json = JsonConvert.SerializeObject(cat, settings);
Assert.Equal("{\"Lives\":6,\"Age\":11,\"Type\":2}", json);
var result = JsonConvert.DeserializeObject<Animal>(json, settings);
Assert.Equal(typeof(Cat), result.GetType());
Assert.Equal(11, result.Age);
Assert.Equal(6, (result as Cat)?.Lives);
[JsonConverter(typeof(JsonSubtypes))]
[JsonSubtypes.KnownSubTypeWithProperty(typeof(Employee), "JobTitle")]
[JsonSubtypes.KnownSubTypeWithProperty(typeof(Artist), "Skill")]
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Employee : Person
{
public string Department { get; set; }
public string JobTitle { get; set; }
}
public class Artist : Person
{
public string Skill { get; set; }
}
or using syntax with generics:
string json = "[{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," +
"{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," +
"{\"Skill\":\"Painter\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}]";
var persons = JsonConvert.DeserializeObject<IReadOnlyCollection<Person>>(json);
Assert.AreEqual("Painter", (persons.Last() as Artist)?.Skill);
settings.Converters.Add(JsonSubtypesWithPropertyConverterBuilder
.Of(typeof(Person))
.RegisterSubtypeWithProperty(typeof(Employee), "JobTitle")
.RegisterSubtypeWithProperty(typeof(Artist), "Skill")
.Build());
or
settings.Converters.Add(JsonSubtypesWithPropertyConverterBuilder
.Of<Person>()
.RegisterSubtypeWithProperty<Employee>("JobTitle")
.RegisterSubtypeWithProperty<Artist>("Skill")
.Build());
[JsonConverter(typeof(JsonSubtypes))]
[JsonSubtypes.KnownSubType(typeof(ConstantExpression), "Constant")]
[JsonSubtypes.FallBackSubType(typeof(UnknownExpression))]
public interface IExpression
{
string Type { get; }
}
Or with code configuration:
settings.Converters.Add(JsonSubtypesConverterBuilder
.Of(typeof(IExpression), "Type")
.SetFallbackSubtype(typeof(UnknownExpression))
.RegisterSubtype(typeof(ConstantExpression), "Constant")
.Build());
settings.Converters.Add(JsonSubtypesWithPropertyConverterBuilder
.Of(typeof(IExpression))
.SetFallbackSubtype(typeof(UnknownExpression))
.RegisterSubtype(typeof(ConstantExpression), "Value")
.Build());
If this project helped you save money or time or simply makes your life also easier, you can give me a cup of coffee =)