Skip to content

Add a Badeend.EnumClass.NewtonsoftJson extension #1

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;
        }
    }
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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