Skip to content

Instantly share code, notes, and snippets.

@davidkeaveny
Last active May 2, 2023 05:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davidkeaveny/ffa24630a953e46d02c4403a66c928a7 to your computer and use it in GitHub Desktop.
Save davidkeaveny/ffa24630a953e46d02c4403a66c928a7 to your computer and use it in GitHub Desktop.
Custom System.Text.Json enum converter
[AttributeUsage(AttributeTargets.Field)]
public class AlternativeValueAttribute : Attribute
{
public AlternativeValueAttribute(string code)
{
Code = code;
}
public string Code { get; }
}
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}'.");
}
}
}
public enum TestEnum
{
[AlternativeValue("F")]
First = 0,
Second = 1
}
public class TestClass
{
public TestEnum Code { get; set; }
public TestEnum? NullableCode { get; set; }
}
[UnitTest]
public class ConverterTest
{
private static readonly JsonSerializerOptions Settings;
static ConverterTest()
{
Settings = new JsonSerializerOptions
{
WriteIndented = false
};
Settings.Converters.Add(new AlternativeValueJsonStringEnumConverter());
}
[Fact]
public void ItShouldSerialiseInLonghand()
{
var input = new TestClass { Code = TestEnum.First, NullableCode = TestEnum.First };
var output = JsonSerializer.Serialize(input, Settings);
output.Should().Be("{\"Code\":\"First\",\"NullableCode\":\"First\"}");
}
[Fact]
public void ItShouldDeserialiseWhenValueIsDecorated()
{
const string input = "{ Code: 'F' }";
var output = JsonSerializer.Deserialize<TestClass>(input, Settings);
output.Should().BeEquivalentTo(new { Code = TestEnum.First });
}
[Fact]
public void ItShouldDeserialiseWhenValueIsDecoratedAndLonghandIsUsed()
{
const string input = "{ Code: 'First' }";
var output = JsonSerializer.Deserialize<TestClass>(input, Settings);
output.Should().BeEquivalentTo(new { Code = TestEnum.First });
}
[Fact]
public void ItShouldDeserialiseWhenValueIsNotDecorated()
{
const string input = "{ Code: 'Second' }";
var output = JsonSerializer.Deserialize<TestClass>(input, Settings);
output.Should().BeEquivalentTo(new { Code = TestEnum.Second });
}
[Fact]
public void ItShouldDeserialiseWhenIntegerIsUsedAndValueIsValid()
{
const string input = "{ Code: 1 }";
var output = JsonSerializer.Deserialize<TestClass>(input, Settings);
output.Should().BeEquivalentTo(new { Code = TestEnum.Second });
}
[Fact]
public void ItShouldNotDeserialiseWhenIntegerIsUsedAndValueIsInvalid()
{
const string input = "{ Code: 2 }";
Action operation = () => JsonSerializer.Deserialize<TestClass>(input, Settings);
operation.Should().Throw<JsonException>().WithMessage("Could not convert '2' to enum of type 'TestEnum'.");
}
[Fact]
public void ItShouldNotDeserialiseWhenNullableValueIsNotSpecified()
{
const string input = "{ Code: 'Second' }";
var output = JsonSerializer.Deserialize<TestClass>(input, Settings);
output.Should().BeEquivalentTo(new { Code = TestEnum.Second });
}
[Fact]
public void ItShouldDeserialiseWhenNullableValueIsSpecified()
{
const string input = "{ Code: 'Second', NullableCode: 'F' }";
var output = JsonSerializer.Deserialize<TestClass>(input, Settings);
output.Should().BeEquivalentTo(new { Code = TestEnum.Second, NullableCode = TestEnum.First });
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment