I am working with an API that handles requests that submit large amounts of data in JSON format (e.g. 10MB+). To date, we have been using Newtonsoft.Json, but recently we have been experiencing performance issues, mostly related to the amount of time and/or memory required to deserialise the request data into the appropriate types, so we have been looking at switching to System.Text.Json, which according to our internal benchmarking is significantly better in both metrics.
Where I am having problems in modifying the existing code is that we have some custom deserialisation logic written around processing enums, in the form of a JSON.NET custom converter. For historical reasons, we have some clients of the API who use a different set of enum values to the values that the API expects! So, our enum might look something like this:
[AttributeUsage(AttributeTargets.Field)]
public class AlternativeValueAttribute : Attribute
{
    public AlternativeValueAttribute(string code) {
        Code = code;
    }
        
    public string Code { get; }
}
public enum Allowances
{
  [AlternativeValue("CD")] Car = 0,
  [AlternativeValue("AD")] Transport = 1,
  [AlternativeValue("LD")] Laundry = 2
}
public class AllowanceRequest
{
  public Allowances Type { get; set; }
  public Allowances? AdditionalType { get; set; }
  public decimal Value { get; set; }
}
so client A will submit their data as:
{ "type": "Car", "value": 25.75 }
and client B will submit their data as:
{ "type": 0, "value": 25.75 }
and client C will submit their data as:
{ "type": "CD", "value": 25.75 }
(I've given a single example here, but there are many enums that make use of this custom attribute, so having an enum-specific converter is not really a valid approach - the converter needs to be fairly generic).
I am having difficulties understanding exactly how System.Text.Json handles custom conversion of enums, as that seems to be a special case and handled differently to a regular class, requiring the use of a JsonConverterFactory instead of an implementation of JsonConverter<>.
public class AlternativeValueJsonStringEnumConverter : JsonConverterFactory {
    public AlternativeValueJsonStringEnumConverter() {}
    public override bool CanConvert(Type typeToConvert) {
        var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
        return enumType.IsEnum;
    }
    public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) {
        return new CustomStringEnumConverter(options);
    }
    private class CustomStringEnumConverter : JsonConverter<Enum?>
    {
        public CustomStringEnumConverter(JsonSerializerOptions options) { }
        public override Enum? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
            var isNullable = Nullable.GetUnderlyingType(typeToConvert) != null;
            var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
            switch (reader.TokenType) {
                case JsonTokenType.Null when !isNullable:
                    throw new JsonException("Cannot deserialise null value to non-nullable field");
                case JsonTokenType.String:
                    var result = ReadStringValue(reader, enumType);
                    return (Enum?) result;
                case JsonTokenType.Number:
                    return ReadNumberValue(reader, enumType);
                default:
                    return null;
            }
        }
        public override void Write(Utf8JsonWriter writer, Enum? value, JsonSerializerOptions options) {
            if (value == null) {
                writer.WriteNullValue();
            } else {
                var description = value.ToString();
                writer.WriteStringValue(description);
            }
        }
        public override bool CanConvert(Type typeToConvert) {
            var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
            return enumType.IsEnum;
        }
        private static string GetDescription(Enum source) {
            var fieldInfo = source.GetType().GetField(source.ToString());
            if (fieldInfo == null) {
                return source.ToString();
            }
            
            var attributes = (System.ComponentModel.DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(System.ComponentModel.DescriptionAttribute), false);
            return attributes != null && attributes.Length > 0
                ? attributes[0].Description
                : source.ToString();
        }
        private static object? ReadStringValue(Utf8JsonReader reader, Type enumType) {
            var parsedValue = reader.GetString()!;
            foreach (var item in Enum.GetValues(enumType))
            {
                var attribute = item.GetType().GetTypeInfo().GetRuntimeField(item.ToString()).GetCustomAttribute<AlternativeValueAttribute>();
                if (attribute == null && Enum.TryParse(enumType, parsedValue, true, out var result)) {
                    return result;
                }
                if (attribute != null && attribute.Code == parsedValue &&
                    Enum.TryParse(enumType, item.ToString(), true, out var attributedResult)) {
                    return attributedResult;
                }
                if (parsedValue == item.ToString() && Enum.TryParse(enumType, parsedValue, true, out var parsedResult)) {
                    return parsedResult;
                }
            }
            return null;
        }
        private static Enum? ReadNumberValue(Utf8JsonReader reader, Type enumType) {
            var result = int.Parse(reader.GetString()!);
            var castResult = Enum.ToObject(enumType, result);
            foreach (var item in Enum.GetValues(enumType)) {
                if (castResult.Equals(item)) {
                    return (Enum?)Convert.ChangeType(castResult, enumType);
                }
            }
            throw new JsonException($"Could not convert '{result}' to enum of type '{enumType.Name}'.");
        }
    }
}
I then run the same tests that I wrote for the JSON.NET implementation, to make sure nothing has broken e.g.:
public class WhenUsingCustomSerialiser
{
    [Fact]
    public void ItShouldDeserialiseWhenValueIsDecorated()
    {
        var settings = new JsonSerializerOptions { WriteIndented = false };
        settings.Converters.Add(new AlternativeValueJsonStringEnumConverter());
        var output = JsonSerializer.Deserialize<AllowanceRequest>("{ Type: 'CD', Value: 25.75 }", settings);
        output.Should().BeEquivalentTo(new { Type = Allowances.Car, Value = 25.75M });
    }
}
But that fails with the following exception:
System.InvalidOperationException
The converter 'AlternativeValueJsonStringEnumConverter+CustomStringEnumConverter' is not compatible with the type 'System.Nullable`1[TestEnum]'.
   at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_SerializationConverterNotCompatible(Type converterType, Type type)
   at System.Text.Json.JsonSerializerOptions.GetConverterInternal(Type typeToConvert)
   at System.Text.Json.JsonSerializerOptions.DetermineConverter(Type parentClassType, Type runtimePropertyType, MemberInfo memberInfo)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.GetConverter(Type type, Type parentClassType, MemberInfo memberInfo, Type& runtimeType, JsonSerializerOptions options)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.AddProperty(MemberInfo memberInfo, Type memberType, Type parentClassType, Boolean isVirtual, Nullable`1 parentTypeNumberHandling, JsonSerializerOptions options)
Does anyone have any experience in writing their own custom enum serialization code?
 
    