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

