-
-
Save tuscen/22597500be33cb7e4edbe8b0bef44905 to your computer and use it in GitHub Desktop.
Configurable JsonConverter for deserializing discriminated JSON
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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."); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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