The problem

We want to use the same approach as we do when storing the enumeration class in the database, we only want to serialize the actual value of the enumeration. We also want to be able to only supply the value when deserializing, we don't want to supply all the other properties.

The following classes will be used for the examples in this post:

public class MyDto
{
    public required Hamburger Hamburger { get; init; }
    public required List<Hamburger> Hamburgers { get; init; }
}

public record Hamburger : Enumeration<Hamburger>
{
    public static Hamburger Cheeseburger = new (1, "Cheeseburger");
    public static Hamburger BigMac = new(2, "Big Mac");
    public static Hamburger BigTasty = new(3, "Big Tasty");

    private Hamburger(int id, string displayName) : base(id, displayName)
    {
    }
}

Out of the box, System.Text.Json will serialize the following object...

var dto = new MyDto
{
    Hamburger = Hamburger.BigMac,
    Hamburgers = new List<Hamburger>(Hamburger.GetAll())
};

...to the following json:

{
  "hamburger": {
    "value": 2,
    "displayName": "Big Mac"
  },
  "hamburgers": [
    {
      "value": 1,
      "displayName": "Cheeseburger"
    },
    {
      "value": 2,
      "displayName": "Big Mac"
    },
    {
      "value": 3,
      "displayName": "Big Tasty"
    }
  ]
}

As you can see, it includes the displayName property, we don't want that, we only want to include the value property.

The solution

The following two tests illustrates what we want to achieve:

[Fact]
public void SerializesCorrectly()
{
    var dto = new MyDto
    {
        Hamburger = Hamburger.BigMac,
        Hamburgers = new List<Hamburger>(Hamburger.GetAll())
    };

    var result = JsonSerializer.Serialize(dto);

    result.ShouldBe("{\"hamburger\":2,\"hamburgers\":[1,2,3]}");
}

[Fact]
public void DeserializesCorrectly()
{
    var json = "{\"hamburger\":2,\"hamburgers\":[1,2,3]}";

    var result = JsonSerializer.Deserialize<MyDto>(json)!;

    result.Hamburger.ShouldBe(Hamburger.BigMac);
    result.Hamburgers.ShouldBe(Hamburger.GetAll());
}

Since we are dealing with System.Text.Json, we need to create a custom JsonConverter. If you want to see a more complex JsonConverter than the one we will create in this post, you can have a look at this one where I create a JsonConverter for a Dictionary<string, object>.

We need to implement the Write and Read method. We will also override the CanConvert method to enable support for all the different enumeration implementations.

The Write method handles the serialization:

public override void Write(Utf8JsonWriter writer, Enumeration<T> value, JsonSerializerOptions options)
{
    // Only write the **Value** property when serializing.
    writer.WriteNumberValue(value.Value);
}

And the Read method handles the deserialization:

public override Enumeration<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    // Read the integer from the JSON and find the correct
    // Enumeration via the FromValue method
    var value = reader.GetInt32();
    return Enumeration<T>.FromValue(value);
}

We also need to override the CanConvert method to support all the different implementations of the enumeration class:

public override bool CanConvert(Type typeToConvert)
{
    return typeToConvert.BaseType == typeof(Enumeration<>).MakeGenericType(typeof(T));
}

The full JsonConverter looks like this:

public class EnumerationJsonConverter<T> : JsonConverter<Enumeration<T>> where T : Enumeration<T>
{
    public override bool CanConvert(Type typeToConvert)
    {
        return typeToConvert.BaseType == typeof(Enumeration<>).MakeGenericType(typeof(T));
    }

    public override Enumeration<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var value = reader.GetInt32();
        return Enumeration<T>.FromValue(value);
    }

    public override void Write(Utf8JsonWriter writer, Enumeration<T> value, JsonSerializerOptions options)
    {
        writer.WriteNumberValue(value.Value);
    }
}

Only thing left to do to make our tests pass is to register our custom JsonConverter...

private readonly JsonSerializerOptions _jsonSerializerOptions;

public EnumerationJsonConverterTests()
{
    _jsonSerializerOptions = new JsonSerializerOptions
    {
        Converters = { new EnumerationJsonConverter<Hamburger>() },
        PropertyNameCaseInsensitive = true,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };
}

...and then make sure to use the JsonSerializerOptions when de/serializing:

[Fact]
public void SerializesCorrectly()
{
    var dto = new MyDto
    {
        Hamburger = Hamburger.BigMac,
        Hamburgers = new List<Hamburger>(Hamburger.GetAll())
    };

    var result = JsonSerializer.Serialize(dto, _jsonSerializerOptions);

    result.ShouldBe("{\"hamburger\":2,\"hamburgers\":[1,2,3]}");
}

[Fact]
public void DeserializesCorrectly()
{
    var json = "{\"hamburger\":2,\"hamburgers\":[1,2,3]}";

    var result = JsonSerializer.Deserialize<MyDto>(json, _jsonSerializerOptions)!;

    result.Hamburger.ShouldBe(Hamburger.BigMac);
    result.Hamburgers.ShouldBe(Hamburger.GetAll());
}

All code in this post can be found over at GitHub.