A JsonConverter that allows implementations for an interface or abstract class to be chosen based on a discriminating property
using Newtonsoft.Json; | |
using Newtonsoft.Json.Linq; | |
using System; | |
using System.Collections.Generic; | |
namespace Utilities | |
{ | |
/// <summary> | |
/// Allows interfaces to be deserialized by creating a concrete implementation, based on some discriminating key in | |
/// the interface. | |
/// </summary> | |
/// <typeparam name="TInterface">The interface type</typeparam> | |
/// <typeparam name="TKey">The key type</typeparam> | |
public class TaggedUnionConverter<TInterface, TKey> : JsonConverter | |
{ | |
/// <summary> | |
/// The field in the interface that defines which implementation to use. | |
/// </summary> | |
public string Discriminator { get; set; } | |
/// <summary> | |
/// A mapping of keys to concrete implementations of <see cref="TInterface"/>. | |
/// </summary> | |
public IDictionary<TKey, Type> Implementations { get; set; } | |
/// <summary> | |
/// Gets a value indicating whether this Newtonsoft.Json.JsonConverter can read JSON. | |
/// </summary> | |
public override bool CanRead => true; | |
/// <summary> | |
/// Gets a value indicating whether this Newtonsoft.Json.JsonConverter can write JSON. | |
/// </summary> | |
public override bool CanWrite => false; | |
/// <summary> | |
/// Constructs a new converter, using the given discriminator and implementations. | |
/// </summary> | |
/// <param name="discriminator">The field in the interface that defines which implementation to use.</param> | |
/// <param name="implementations">A mapping of keys to concrete implementations of <see cref="TInterface"/>.</param> | |
public TaggedUnionConverter(string discriminator, IDictionary<TKey, Type> implementations) | |
{ | |
Discriminator = discriminator ?? throw new ArgumentNullException(nameof(discriminator)); | |
Implementations = implementations ?? throw new ArgumentNullException(nameof(implementations)); | |
} | |
/// <summary> | |
/// Determines whether this instance can convert the specified object type. | |
/// </summary> | |
/// <param name="objectType">The object type</param> | |
/// <returns>true if this instance can convert the specified object type; otherwise, false.</returns> | |
public override bool CanConvert(Type objectType) | |
{ | |
return objectType == typeof(TInterface); | |
} | |
/// <summary> | |
/// Reads the JSON representation of the object, using the discriminator to determine the appropriate type. If | |
/// the discriminator is not present, or the value cannot be converted to the key type, or the value is not | |
/// contained in the set of implementations, an exception will be thrown, and the target object will be left | |
/// with its default value. | |
/// </summary> | |
/// <param name="reader">The Newtonsoft.Json.JsonReader to read from.</param> | |
/// <param name="objectType">Type of the object.</param> | |
/// <param name="existingValue">The existing value of object being read.</param> | |
/// <param name="serializer">The calling serializer.</param> | |
/// <returns>The object value.</returns> | |
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |
{ | |
if (reader.TokenType == JsonToken.Null) | |
{ | |
reader.Read(); | |
return null; | |
} | |
var jsonObject = JObject.Load(reader); | |
if (!jsonObject.TryGetValue(Discriminator, out var discriminator)) | |
throw new InvalidOperationException("The model does not contain a value for " + Discriminator + ", so the type cannot be determined"); | |
var value = discriminator.ToObject<TKey>(serializer); | |
if (value == null) | |
throw new InvalidOperationException("The value '" + discriminator + "' is not a valid '" + typeof(TKey).Name + "', so the type cannot be determined"); | |
if (!Implementations.TryGetValue(value, out var type)) | |
throw new InvalidOperationException("The value '" + discriminator + "' is not a valid value for '" + Discriminator + "', so the type cannot be determined"); | |
return jsonObject.ToObject(type, serializer); | |
} | |
/// <summary> | |
/// Writes the JSON representation of the object. Calling this method will result in an exception, as it is not | |
/// a valid operation. | |
/// </summary> | |
/// <param name="writer">The Newtonsoft.Json.JsonWriter to write to.</param> | |
/// <param name="value">The value.</param> | |
/// <param name="serializer">The calling serializer.</param> | |
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | |
{ | |
throw new InvalidOperationException("Use default serialization."); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment