Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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