-
-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
Problem
Users of Newtonsoft.Json cannot benefit from the automatic polymorphic behavior like System.Text.Json users can (via Badeend.EnumClass.SystemTextJson).
Example solution
Program.cs usage:
services.Configure<MvcNewtonsoftJsonOptions>(options =>
{
options.SerializerSettings.Converters.Add(new EnumClassPolymorphicConverter());
});Example implementation:
public class EnumClassPolymorphicConverter : JsonConverter
{
private static readonly ConcurrentDictionary<Type, JsonConverter> _converterCache = new();
public override bool CanConvert(Type objectType)
{
var baseType = objectType.BaseType ?? objectType;
return baseType.GetCustomAttributeInInheritanceChain<Badeend.EnumClassAttribute>() != null;
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}
var converter = GetOrCreateConverter(value.GetType().BaseType ?? value.GetType());
converter.WriteJson(writer, value, serializer);
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
var converter = GetOrCreateConverter(objectType);
return converter.ReadJson(reader, objectType, existingValue, serializer);
}
private static JsonConverter GetOrCreateConverter(Type type)
{
return _converterCache.GetOrAdd(type, t =>
{
var genericConverterType = typeof(EnumClassPolymorphicConverter<>).MakeGenericType(t);
return (JsonConverter)Activator.CreateInstance(genericConverterType)!;
});
}
}
public class EnumClassPolymorphicConverter<TBase> : JsonConverter<TBase>
{
private readonly string _typeDiscriminatorProperty = "$type";
private readonly Dictionary<string, Type> _discriminatorToTypeMap = new();
private readonly Dictionary<Type, string> _typeToDiscriminatorMap = new();
public EnumClassPolymorphicConverter()
{
var baseType = typeof(TBase);
_ = baseType.GetCustomAttributeInInheritanceChain<Badeend.EnumClassAttribute>() ?? throw new InvalidOperationException($"{baseType} is not marked with [EnumClass]");
var derivedTypes = baseType.GetEnumClassCases();
foreach (var derivedType in derivedTypes)
{
var discriminator = GetDiscriminator(derivedType);
_discriminatorToTypeMap[discriminator] = derivedType;
_typeToDiscriminatorMap[derivedType] = discriminator;
}
}
private string GetDiscriminator(Type type)
{
var attr = type.GetCustomAttribute<Badeend.EnumClass.SystemTextJson.JsonDiscriminatorAttribute>(false);
if (attr != null)
return attr.Discriminator?.ToString() ?? throw new InvalidOperationException("Discriminator cannot be null");
return type.Name;
}
public override void WriteJson(JsonWriter writer, TBase? value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}
var newSerializer = serializer.Copy();
foreach (var conv in serializer.Converters)
{
if (conv.GetType() != typeof(EnumClassPolymorphicConverter))
newSerializer.Converters.Add(conv);
}
var jo = JObject.FromObject(value, newSerializer);
if (_typeToDiscriminatorMap.TryGetValue(value.GetType(), out var discriminator))
{
if (jo.ContainsKey(_typeDiscriminatorProperty))
jo[_typeDiscriminatorProperty] = discriminator;
else
jo.AddFirst(new JProperty(_typeDiscriminatorProperty, discriminator));
}
jo.WriteTo(writer);
}
public override TBase? ReadJson(JsonReader reader, Type objectType, TBase? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var jo = JObject.Load(reader);
var discriminator = jo[_typeDiscriminatorProperty]?.ToString();
if (discriminator != null && _discriminatorToTypeMap.TryGetValue(discriminator, out var concreteType))
{
return (TBase?)jo.ToObject(concreteType, serializer);
}
throw new JsonSerializationException($"Unknown discriminator '{discriminator}' for base type {typeof(TBase).Name}");
}
}
public static class Extensions
{
public static JsonSerializer Copy(this JsonSerializer serializer)
{
var newSerializer = new JsonSerializer
{
Context = serializer.Context,
Culture = serializer.Culture,
ContractResolver = serializer.ContractResolver,
ConstructorHandling = serializer.ConstructorHandling,
CheckAdditionalContent = serializer.CheckAdditionalContent,
DateFormatHandling = serializer.DateFormatHandling,
DateFormatString = serializer.DateFormatString,
DateParseHandling = serializer.DateParseHandling,
DateTimeZoneHandling = serializer.DateTimeZoneHandling,
DefaultValueHandling = serializer.DefaultValueHandling,
EqualityComparer = serializer.EqualityComparer,
FloatFormatHandling = serializer.FloatFormatHandling,
Formatting = serializer.Formatting,
FloatParseHandling = serializer.FloatParseHandling,
MaxDepth = serializer.MaxDepth,
MetadataPropertyHandling = serializer.MetadataPropertyHandling,
MissingMemberHandling = serializer.MissingMemberHandling,
NullValueHandling = serializer.NullValueHandling,
ObjectCreationHandling = serializer.ObjectCreationHandling,
PreserveReferencesHandling = serializer.PreserveReferencesHandling,
ReferenceResolver = serializer.ReferenceResolver,
ReferenceLoopHandling = serializer.ReferenceLoopHandling,
StringEscapeHandling = serializer.StringEscapeHandling,
TraceWriter = serializer.TraceWriter,
TypeNameHandling = serializer.TypeNameHandling,
SerializationBinder = serializer.SerializationBinder,
TypeNameAssemblyFormatHandling = serializer.TypeNameAssemblyFormatHandling
};
return newSerializer;
}
public static T? GetCustomAttributeInInheritanceChain<T>(this Type? type)
where T : Attribute
{
while (type != null && type != typeof(object))
{
var attr = type.GetCustomAttribute<T>(inherit: false);
if (attr != null)
return attr;
type = type.BaseType ?? type.DeclaringType;
}
return null;
}
public static IEnumerable<T> GetCustomAttributesInInheritanceChain<T>(this Type? type)
where T : Attribute
{
while (type != null && type != typeof(object))
{
foreach (var attr in type.GetCustomAttributes<T>())
{
yield return attr;
}
type = type.BaseType;
}
}
}Metadata
Metadata
Assignees
Labels
No labels