DEV Community

Amin Khansari
Amin Khansari

Posted on

Mapping > Json Converters // true

Let's imagine that we have a cool domain with some special types.
For instance, in our very simple example, we have a strongly-typed ID to get rid of primitive obsession.

public record struct CoolId(int Value);

public class Cool
{
  public CoolId CoolId { get; set; }
  public string Title { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Nice! OK, now let's create an API for this.
Amazing how easy ASP.NET Core is.

var builder = WebApplication.CreateBuilder();

builder.Services.Configure<JsonOptions>(o =>
{
  o.SerializerOptions.WriteIndented = true;
});

var app = builder.Build();

app.MapGet("/cool", () =>
  new Cool { CoolId = new CoolId(123), Title = "Very Cool!" });

app.Run();
Enter fullscreen mode Exit fullscreen mode

The concern is that CoolId is not correctly serialized and it should be simplified.

{
  "coolId": {
    "value": 123
  },
  "title": "Very Cool!"
}
Enter fullscreen mode Exit fullscreen mode

So, the first reaction is to create a JsonConverter for it.
Hmm, why not.

public class CoolIdJsonConverter : JsonConverter<CoolId>
{
  public override CoolId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
    new CoolId(reader.GetInt32());

  public override void Write(Utf8JsonWriter writer, CoolId value, JsonSerializerOptions options) =>
    writer.WriteNumberValue(value.Value);
}
Enter fullscreen mode Exit fullscreen mode
builder.Services.Configure<JsonOptions>(o =>
{
  o.SerializerOptions.WriteIndented = true;
  o.SerializerOptions.Converters.Add(new CoolIdJsonConverter());
});
Enter fullscreen mode Exit fullscreen mode

And voila!

{
  "coolId": 123,
  "title": "Very Cool!"
}
Enter fullscreen mode Exit fullscreen mode

Drawbacks

The big issue with this approach and custom serializers in general is that the entire logic is hidden in the wilds of the serializer library.
This leads to the following problems:

  • Very bad model discovery. It's pretty hard to read and guess the model from the custom serializer. Especially in big payloads when the construction depends on many logics.
  • Highly coupled with the serializer library.
    • Not compatible with OpenAPI/Swagger generation.
    • Logic duplication if we want to provide other formats such as Xml or Yaml.
    • Main blocking point when we want to switch to another library. For instance Json.Net to System.Text.Json.
    • Not convenient to test raw strings.
  • Reflection hell.
  • We usually end up creating a client library (NuGet) for the API which is a bad practice and the opposite of the OpenAPI and Restful philosophy.

Mapping > Custom Serialization

With a simple mapping we can fix all the previous drawbacks.

public class CoolModel
{
  public int CoolId { get; set; }
  public string Title { get; set; }
}

public static class CoolModelMapper
{
  public static Cool ToDomain(this CoolModel model) => new Cool
  {
    CoolId = new CoolId(model.CoolId),
    Title = model.Title,
  };

  public static CoolModel ToModel(this Cool domain) => new CoolModel
  {
    CoolId = domain.CoolId.Value,
    Title = domain.Title,
  };
}
Enter fullscreen mode Exit fullscreen mode

What is handy with this pattern is the possibility to make a composition of ToModel or ToDomain. For instance, we can replace domain.CoolId.Value with domain.CoolId.ToModel() and so forth.

app.MapGet("/", () =>
    new Cool { CoolId = new CoolId(123), Title = "Very Cool!" }
    .ToModel());
Enter fullscreen mode Exit fullscreen mode

Conclusion

JSON is simple, and so should its model.

The examples were in C# but it's also true for F# or any other languages.

Top comments (0)