Hyston blog
About • RSS

JsonConverter and enum flags

2021-06-06 15:02

It is beginning of June and it means, that time for first post of the year has become. Enum flags are useful wrapper around bitmasks and can be used to store several flags into one field/property. Internally they are just integers where each bit represent a flag state and can be set on or off. For example, lets define what next weekend should look like by writing down possible plans:

[Flags]
public enum WeekendState
{
    NONE = 0,
    SEX = 1 << 0,
    DRUGS = 1 << 1,
    ROCK = 1 << 2,
    ROLL = 1 << 3,
    DONUTS = 1 << 4
}

Here each state have their own 1 bit in place and can be combined with every other one. By adding Flags attribute we tell .net to treat this enum as a bitset.

And let’s make simple controller, that will predict our (hardcoded) future:

[ApiController]
[Route("[controller]")]
public class WeekendController : ControllerBase
{
	[HttpGet]
  public WeekendState Get() =>
	WeekendState.DRUGS | WeekendState.DONUTS;
}

If we call this controller we will get 18. Donuts is 10000 aka 16, Drugs is 10 aka 2. 2 + 16 = 18, easy. But if you don’t see backend code it can be not so easy. I’m asking second main question of the universe - what should I do on weekend and I’m getting 18? What doesn’t this mean? 1

We need to serialise enum flags as strings. Easiest way to do that - add json serialised options in Startup.

services
    .AddControllers()
    .AddJsonOptions(options => 
        options
            .JsonSerializerOptions
            .Converters.Add(new JsonStringEnumConverter())
        );

Here JsonStringEnumConverter is default way of enum serialization. After these changes our little endpoint will return “DRUGS, DONUTS” which is an improvement. But frontend (or whoever will use api) may not like string with separated with comma values. The most logical way would be to return the array. To do that we need to write our own JsonConverter.

public class FlagJsonConverter<T> : JsonConverter<T> where T : struct, Enum
{
    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        //TODO
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        //TODO
    }
}

And use it by default for type WeekendState :

services
    .AddControllers()
    .AddJsonOptions(options => 
    {
        options
            .JsonSerializerOptions
            .Converters.Add(new FlagJsonConverter<WeekendState>());
        options
            .JsonSerializerOptions
            .Converters.Add(new JsonStringEnumConverter());
    });

Note: Order here is important, first passable converter will be used.

First method in our new converter is Read, which takes values from Utf8JsonReader and does everything possible to convert them into required enum (converter class is generic and can be used for all enums).

var derserialised = JsonSerializer.Deserialize<string[]>(ref reader);
var textValue = string.Join(',', derserialised ?? new string[0]);

return Enum.TryParse<T>(textValue, ignoreCase: true, out T result)
        ? result
        : (T)Enum.Parse(typeToConvert, "0");
}

Input string for that is parsed json token, which we convert to string array (if possible) and then join back as comma-separated strings. May be there are more elegant solution, but this was a fastest to code. And then this string is parsed using default parser. If failed - it tries to parse zero value, which lead to simplest result (WeekendState.None).

Another function is Write:

writer?.WriteStartArray();
var values = Enum.GetValues<T>().Where(e => value.HasFlag(e));
foreach (var val in values)
        writer.WriteStringValue(val.ToString());
writer?.WriteEndArray();
}

This is taking existing value, iterate it into all assigned flags and convert them into strings. And, on the way wrap it into array.

And right now result of our beautiful endpoint is:

[
  "DRUGS",
  "DONUTS"
]

That was exactly what I need.

And for final step here are xunit tests for deserialisation:

[Theory]
[InlineData("[\"DONUTS\"]", WeekendState.DONUTS)]
[InlineData("[\"ROCK\", \"ROLL\"]", WeekendState.ROCK|WeekendState.ROLL)]
[InlineData("[]", WeekendState.NONE)]
public void TestDeserialisation(string input, WeekendState result)
{
    var utf8JsonReader = new Utf8JsonReader(Encoding.UTF8.GetBytes(input), false, new JsonReaderState(new JsonReaderOptions()));
    var converter = new FlagJsonConverter<WeekendState>();
    var value = converter.Read(ref utf8JsonReader, typeof(WeekendState), new JsonSerializerOptions());
    Assert.Equal(value, result);
}

Bonus: spending each weekend eating donuts and drugs can be tedious. Let’s update controller code:

[HttpGet]
public WeekendState Get() 
{
    Random rnd = new Random();
    var shuffled = Enum.GetValues<WeekendState>().OrderBy(c => rnd.Next());
    var values = shuffled.Take(rnd.Next(shuffled.Count()));
    var result = WeekendState.NONE;
    foreach (var val in values)
        result |= val;
    return result;
}

It always contain random amount of random items. It also always include NONE value, which is not a proper solution.


  1. Probably, the Answer to the Ultimate Question of Life, the Universe, and Everything is three parts: 32 + 8 + 2, we just don’t know the name of the proper Enum.


Next entry → ← Prev entry