Last active
May 2, 2023 05:33
-
-
Save davidkeaveny/ffa24630a953e46d02c4403a66c928a7 to your computer and use it in GitHub Desktop.
Custom System.Text.Json enum converter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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