1

I would like to serialise FlagsAttribute enum's as an array using System.Text.Json.

When using the built-in JsonStringEnumConverter these type of enum's are serialised as comma separated strings e.g. "Red, Black".

I would prefer serialising as ["Red", "Black"].

Daniel
  • 717
  • 6
  • 21

1 Answers1

3

Implement a custom converter and factory as follows:

/// <summary>
/// JSON serialization factory for `[Flags]` based `enum's` as `string[]`
/// </summary>
/// <see href="https://stackoverflow.com/a/59430729/5219886">based on this model</see>
public class EnumWithFlagsJsonConverterFactory : JsonConverterFactory
{
  public EnumWithFlagsJsonConverterFactory() { }

  public override bool CanConvert(Type typeToConvert)
  {
    // https://github.com/dotnet/runtime/issues/42602#issue-706711292
    return typeToConvert.IsEnum && typeToConvert.IsDefined(typeof(FlagsAttribute), false);
  }

  public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
  {
    var converterType = typeof(EnumWithFlagsJsonConverter<>).MakeGenericType(typeToConvert);
    return (JsonConverter)Activator.CreateInstance(converterType);
  }
}

/// <summary>
/// JSON serialization for `[Flags]` based `enum's` as `string[]`
/// </summary>
/// <see href="https://github.com/dotnet/runtime/issues/31081#issuecomment-848697673">based on this model</see>
public class EnumWithFlagsJsonConverter<TEnum> : JsonConverter<TEnum> where TEnum : struct, System.Enum
{
  private readonly Dictionary<TEnum, string> _enumToString = new Dictionary<TEnum, string>();
  private readonly Dictionary<string, TEnum> _stringToEnum = new Dictionary<string, TEnum>();

  public EnumWithFlagsJsonConverter()
  {
    var type = typeof(TEnum);
    var values = System.Enum.GetValues<TEnum>();

    foreach (var value in values)
    {
      var enumMember = type.GetMember(value.ToString())[0];
      var attr = enumMember.GetCustomAttributes(typeof(EnumMemberAttribute), false)
        .Cast<EnumMemberAttribute>()
        .FirstOrDefault();

      _stringToEnum.Add(value.ToString(), value);

      if (attr?.Value != null)
      {
        _enumToString.Add(value, attr.Value);
        _stringToEnum.Add(attr.Value, value);
      }
      else
      {
        _enumToString.Add(value, value.ToString());
      }
    }
  }

  public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  {
    switch (reader.TokenType)
    {
      case JsonTokenType.Null:
        return default(TEnum);
      case JsonTokenType.StartArray:
        TEnum ret = default(TEnum);
        while (reader.Read())
        {
          if (reader.TokenType == JsonTokenType.EndArray)
            break;
          var stringValue = reader.GetString();
          if (_stringToEnum.TryGetValue(stringValue, out var _enumValue))
          {
            ret = Or(ret, _enumValue);
          }
        }
        return ret;
      default:
        throw new JsonException();
    }
  }

  public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
  {
    var values = System.Enum.GetValues<TEnum>();
    writer.WriteStartArray();
    foreach (var _value in values)
    {
      if (value.HasFlag(_value))
      {
        var v = Convert.ToInt32(_value);
        if (v == 0)
        {
          // handle "0" case which HasFlag matches to all values
          // --> only write "0" case if it is the only value present
          if (value.Equals(_value))
          {
            writer.WriteStringValue(_enumToString[_value]);
          }
        }
        else
        {
          writer.WriteStringValue(_enumToString[_value]);
        }
      }
    }
    writer.WriteEndArray();
  }

  /// <summary>
  /// Combine two enum flag values into single enum value.
  /// </summary>
  // <see href="https://stackoverflow.com/a/24172851/5219886">based on this SO</see>
  static TEnum Or(TEnum a, TEnum b)
  {
    if (Enum.GetUnderlyingType(a.GetType()) != typeof(ulong))
      return (TEnum)Enum.ToObject(a.GetType(), Convert.ToInt64(a) | Convert.ToInt64(b));
    else
      return (TEnum)Enum.ToObject(a.GetType(), Convert.ToUInt64(a) | Convert.ToUInt64(b));
  }
}

Samples usage as follows:

var options = new JsonSerializerOptions() {
  WriteIndented = true,
};
options.Converters.Add(
  new EnumWithFlagsJsonConverterFactory()
);
var json2 = JsonSerializer.Serialize(<...>, options);

References:

  1. https://github.com/dotnet/runtime/issues/31081#issuecomment-848697673
  2. https://stackoverflow.com/a/59430729/5219886
  3. https://stackoverflow.com/a/24172851/5219886
  4. https://github.com/dotnet/runtime/issues/42602#issue-706711292
Daniel
  • 717
  • 6
  • 21
  • Probably makes sense to cache `System.Enum.GetValues()` for performance, rather than using `var values = ...`. Also an enum can be `Int64` – Charlieface Jun 26 '22 at 16:03