Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save tuscen/22597500be33cb7e4edbe8b0bef44905 to your computer and use it in GitHub Desktop.
Save tuscen/22597500be33cb7e4edbe8b0bef44905 to your computer and use it in GitHub Desktop.
Configurable JsonConverter for deserializing discriminated JSON
using System;
using System.Diagnostics;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Infrastructure
{
public sealed class DiscriminatedJsonConverter : JsonConverter
{
private readonly DiscriminatorOptions _discriminatorOptions;
public DiscriminatedJsonConverter(Type concreteDiscriminatorOptionsType)
: this((DiscriminatorOptions) Activator.CreateInstance(concreteDiscriminatorOptionsType))
{
}
public DiscriminatedJsonConverter(DiscriminatorOptions discriminatorOptions)
{
_discriminatorOptions = discriminatorOptions ?? throw new ArgumentNullException(nameof(discriminatorOptions));
}
public override bool CanConvert(Type objectType) => _discriminatorOptions.BaseType.IsAssignableFrom(objectType);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}
var json = JObject.Load(reader);
var discriminatorField = json.Property(_discriminatorOptions.DiscriminatorFieldName);
if (discriminatorField is null)
{
if (serializer.TraceWriter?.LevelFilter >= TraceLevel.Error)
{
serializer.TraceWriter.Trace(TraceLevel.Error,
$"Could not find discriminator field '{_discriminatorOptions.DiscriminatorFieldName}'.",
null);
}
throw new JsonSerializationException($"Could not find discriminator field with name '{_discriminatorOptions.DiscriminatorFieldName}'.");
}
var discriminatorFieldValue = discriminatorField.Value.ToString();
if (serializer.TraceWriter?.LevelFilter >= TraceLevel.Info)
{
serializer.TraceWriter.Trace(TraceLevel.Info,
$"Found discriminator field '{discriminatorField.Name}' with value '{discriminatorFieldValue}'.",
null);
}
var found = _discriminatorOptions.GetDiscriminatedTypes().FirstOrDefault(tuple => tuple.TypeName == discriminatorFieldValue).Type;
if (found == null)
{
found = objectType;
if (serializer.TraceWriter?.LevelFilter >= TraceLevel.Warning)
{
serializer.TraceWriter.Trace(TraceLevel.Warning,
$"Discriminator value '{discriminatorFieldValue}' has no corresponding Type. Continuing anyway with Type '{objectType}'.",
null);
}
}
else
{
if (serializer.TraceWriter?.LevelFilter >= TraceLevel.Warning)
{
serializer.TraceWriter.Trace(TraceLevel.Info, $"Discriminator value '{discriminatorFieldValue}' was used to select Type '{found}'.", null);
}
}
_discriminatorOptions.Preprocessor?.Invoke(discriminatorFieldValue, json);
if (!_discriminatorOptions.SerializeDiscriminator)
{
// Remove the discriminator field from the JSON for two possible reasons:
// 1. the user doesn't want to copy the discriminator value from JSON to the CLR object, only the other way around
// 2. the CLR object doesn't even have a discriminator property, in which case MissingMemberHandling.Error would throw
discriminatorField.Remove();
}
// There might be a different converter on the 'found' type
// Use Deserialize to let Json.NET choose the next converter
// Use Populate to ignore any remaining converters (prevents recursion when the next converter is the same as this)
if (found != objectType && found.CustomAttributes.Any(attribute => attribute.AttributeType == typeof(JsonConverterAttribute)))
{
return serializer.Deserialize(json.CreateReader(), found);
}
var value = _discriminatorOptions.Activator?.Invoke(found) ?? Activator.CreateInstance(found);
serializer.Populate(json.CreateReader(), value);
return value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotSupportedException("DiscriminatedJsonConverter should only be used while deserializing.");
}
}
}
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace Infrastructure
{
public delegate object CustomObjectCreator(Type discriminatedType);
public delegate void JsonPreprocessor(string discriminator, JObject jsonObject);
/// <summary>
/// Extend this class to configure a type with a discriminator field.
/// </summary>
public abstract class DiscriminatorOptions
{
/// <summary>Gets the base type, which is typically (but not necessarily) abstract.</summary>
public abstract Type BaseType { get; }
/// <summary>Gets the name of the discriminator field.</summary>
public abstract string DiscriminatorFieldName { get; }
/// <summary>Returns true if the discriminator should be serialized to the CLR type; otherwise false.</summary>
public abstract bool SerializeDiscriminator { get; }
/// <summary>Gets the mappings from discriminator values to CLR types.</summary>
public abstract IEnumerable<(string TypeName, Type Type)> GetDiscriminatedTypes();
/// <summary>Callback that creates an object which will then be populated by the serializer.</summary>
public CustomObjectCreator Activator { get; protected set; } = null;
/// <summary>Callback that can optionally mutate the JObject before it is converted.</summary>
public JsonPreprocessor Preprocessor { get; protected set; } = null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment