Serializing an enum with System.Text.Json
Turns out that json serialized enums with System.Text.Json is pretty annoying. I just wanted the enum serialization/deserialization to work, but they don’t. Here is how I expected them to work.
- Serialize to string.
Not Met: They serialize to a number by default. - Deserialize from string.
Not Met: They deserialize only from numbers by default and throw an exception if a string is passed in. - Deserialize from case-insensitive string.
Not Met: They deserialize only from numbers by default and throw an exception if a string is passed in. - Deserialize from a number.
Met: This worked.
So, I wrote (with the help of AI) a Json enum converter that does this. It took a few classes
using System.Text.Json.Serialization; namespace Rhyous.Serialization; /// <summary>A JsonConverter factory.</summary> public static class EnumJsonConverterFactory { /// <summary>Creates a JsonConverter.</summary> /// <typeparam name="T">The type of JsonConverter to create.</typeparam> /// <param name="useCamelCase">Whether to use camel case or not.</param> /// <returns>The JsonConverter.</returns> public static JsonConverter CreateConverter<T>(bool useCamelCase) where T : struct, Enum { return new JsonStringOrNumberEnumConverter<T>(useCamelCase); } }
using Session.Extensions; using System.Collections.Concurrent; using System.Reflection; using System.Runtime.Serialization; using System.Text.Json; using System.Text.Json.Serialization; namespace Rhyous.Serialization; /// <summary>Custom JsonConverter for enums to handle both string and numeric representations, including support for EnumMemberAttribute.</summary> /// <typeparam name="T">The enum type to be converted.</typeparam> public class JsonStringOrNumberEnumConverter<T> : JsonConverter<T> where T : struct, Enum { private static readonly ConcurrentDictionary<Type, ConcurrentDictionary<string, T>> _enumMemberCache = new(); private readonly bool _camelCase; /// <summary>The constuctor.</summary> /// <param name="camelCase"></param> public JsonStringOrNumberEnumConverter(bool camelCase = false) { _camelCase = camelCase; } /// <summary>Reads and converts the JSON to the enum value.</summary> /// <param name="reader">The Utf8JsonReader.</param> /// <param name="typeToConvert">The type to convert.</param> /// <param name="options">The JsonSerializerOptions.</param> /// <returns>The enum value.</returns> public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) { var enumText = reader.GetString(); if (TryGetEnumValue(enumText, out T value)) { return value; } } else if (reader.TokenType == JsonTokenType.Number) { if (reader.TryGetInt32(out int intValue)) { if (Enum.IsDefined(typeof(T), intValue)) { return (T)(object)intValue; } } } throw new JsonException($"Unable to convert json value to enum {typeToConvert}."); } /// <summary>Writes the enum value as a string.</summary> /// <param name="writer">The Utf8JsonWriter.</param> /// <param name="value">The value to write.</param> /// <param name="options">The JsonSerializerOptions.</param> public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { var enumMemberValue = GetEnumMemberValue(value); writer.WriteStringValue(enumMemberValue); } private static bool TryGetEnumValue(string? value, out T enumValue) { if (value == null) { enumValue = default; return false; } var enumType = typeof(T); var enumMembers = _enumMemberCache.GetOrAdd(enumType, type => { var members = new ConcurrentDictionary<string, T>(StringComparer.OrdinalIgnoreCase); foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static)) { var enumMemberAttr = field.GetCustomAttribute<EnumMemberAttribute>(); var enumValue = (T)field.GetValue(null)!; // An enum can't be null if (enumMemberAttr != null && enumMemberAttr.Value != null) { members.TryAdd(enumMemberAttr.Value, enumValue); } else { members.TryAdd(field.Name, enumValue!); } // Add the numeric value as a string var enumNumericValue = Convert.ToInt32(enumValue).ToString(); members.TryAdd(enumNumericValue, enumValue); } return members; }); return enumMembers.TryGetValue(value, out enumValue); } private string GetEnumMemberValue(T value) { var enumType = typeof(T); var field = enumType.GetField(value.ToString()); if (field == null) { throw new InvalidOperationException($"Enum value {value} not found in {enumType}."); } var enumMemberAttr = field.GetCustomAttribute<EnumMemberAttribute>(); if (enumMemberAttr != null && enumMemberAttr.Value != null) { return enumMemberAttr.Value; } return _camelCase ? ToCameCase(value.ToString()) : value.ToString(); } private string ToCameCase(string value) { return char.ToLowerInvariant(value[0]) + value.Substring(1); } }
using System.Runtime.Serialization; using System.Text.Json.Serialization; namespace Rhyous.Serialization; /// <summary>Custom JsonConverter Attribute for enums to handle both string and numeric representations, including support for <see cref="EnumMemberAttribute"/>, /// and for writing enum values in camel case.</summary> /// <remarks>If <see cref="EnumMemberAttribute"/> is used, it is output as is and the camel case setting is ignored.</remarks> [AttributeUsage(AttributeTargets.Enum)] public class JsonStringOrNumberEnumConverterAttribute : JsonConverterAttribute { private readonly bool _camelCase; /// <summary>The constructor.</summary> /// <param name="camelCase">Whether to use camel case or not.</param> public JsonStringOrNumberEnumConverterAttribute(bool camelCase = false) { _camelCase = camelCase; } /// <summary>Creates the converter.</summary> /// <param name="typeToConvert">The type fo convert.</param> /// <returns></returns> /// <exception cref="InvalidOperationException"></exception> public override JsonConverter CreateConverter(Type typeToConvert) { var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert; var isNullable = Nullable.GetUnderlyingType(typeToConvert) != null; if (!enumType.IsEnum) { throw new InvalidOperationException("JsonStringOrNumberEnumConverter can only be used with enum types."); } var createMethod = typeof(EnumJsonConverterFactory) .GetMethod(nameof(EnumJsonConverterFactory.CreateConverter))! .MakeGenericMethod(enumType); return (JsonConverter)createMethod!.Invoke(null, new object[] { _camelCase })!; } }