Skip to content

Add a Badeend.EnumClass.NewtonsoftJson extension #1

@0x326

Description

@0x326

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

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions