JSON.NET converter for type hierarchies

By default, JSON.NET supports serialization of type hierarchies – or, more generically, runtime types different from the ones declared in the respective properties – through the usage of TypeNameHandling. The serializer can add a $type property whose value is the fully qualified name of the serialized type. While this works, it can be a problem for interop and code maintainability.  There are ways to customize the type name that gets serialize but you still get stuck with the $type and global resolution of types from the name that is serialized (possible collisions if one isn’t careful).

I wrote the following converter that handles a class hierarchy by adding a discriminator property with configurable name and values.


class SubTypesConverter<T> : JsonConverter<T> where T: new()
{
[ThreadStatic]
private static bool isWriting;
private readonly string discriminatorName;
private readonly Dictionary<string, Func<T>> factories;
private readonly Dictionary<Type, string> discriminators;
public override bool CanRead => true;
public override bool CanWrite => !isWriting;
public SubTypesConverter(string discriminatorName)
{
this.discriminatorName = discriminatorName;
this.factories = new Dictionary<string, Func<T>>();
this.discriminators = new Dictionary<Type, string>();
var types = typeof(T).Assembly
.GetTypes()
.Where(t => typeof(T).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var t in types)
{
var discriminator = this.GetDiscriminator(t);
this.factories.Add(discriminator, CreateFactory(t));
this.discriminators.Add(t, discriminator);
}
}
public override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (hasExistingValue)
{
throw new NotSupportedException($"{nameof(SubTypesConverter<T>)} does not allow reading into an existing instance");
}
var jsonObject = JObject.Load(reader);
var discriminator = jsonObject[this.discriminatorName].Value<string>();
var value = this.factories[discriminator]();
serializer.Populate(jsonObject.CreateReader(), value);
return value;
}
public override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer)
{
try
{
isWriting = true;
var jsonObject = JObject.FromObject(value, serializer);
jsonObject.AddFirst(new JProperty(this.discriminatorName, this.discriminators[value.GetType()]));
jsonObject.WriteTo(writer);
}
finally
{
isWriting = false;
}
}
protected virtual string GetDiscriminator(Type type)
{
return type.Name;
}
private static Func<T> CreateFactory(Type t)
{
var newExp = Expression.New(t.GetConstructor(Type.EmptyTypes));
return Expression.Lambda<Func<T>>(newExp).Compile();
}
}

You just need to create an instance passing the base class of your hierarchy as the generic argument and add it to JsonSerializerSettings.Converters. By default the converter uses the type name as the value of the discriminator, but you can change this by subclassing and overriding the GetDiscriminator method. Also, it assumes that all the types in the hierarchy starting in are in the same assembly as T.

Hope this helps!

Advertisement