Fine-Tuning JSON Serialization in .NET

A small guide to custom JSON converters

Tagged with: csharp, programming

Published on

Working with JSON in .NET is pretty easy with the types provided by System.Text.Json. Nevertheless, it is sometimes necessary to implement custom converters to serialize or deserialize some structures. To illustrate this, take the following example where I declare some simple types that are used to represent money.

public sealed record Money(Amount Amount, Currency Currency);
public sealed record Amount(decimal Value);
public sealed record Currency(string Value);

In order to serialize an instance of Money to JSON, the JsonSerializer.Serialize method from the System.Text.Json namespace can be used.

using System.Text.Json;

var oneEur = new Money(new Amount(1), new Currency("EUR"));
var options = new JsonSerializerOptions { WriteIndented = true };
var json = JsonSerializer.Serialize(oneEur, options);

The JSON looks like this:

{
  "Amount": {
    "Value": 1
  },
  "Currency": {
    "Value": "EUR"
  }
}

The produced JSON is nested two levels deep because of the Amount and Currency records. Neither is this efficient nor is it especially pretty. I would like the JSON to look like this:

{
  "Amount": 1,
  "Currency": "EUR"
}

To accomplish this, I need to implement one JSON converter for Amount and another one for Currency. A JSON converter can be created by inheriting from the JsonConverter<T> class and implementing the Write and Read method. The converters for Amount and Currency can be seen in the next example.

using System.Text.Json;
using System.Text.Json.Serialization;

public sealed class AmountJsonConverter : JsonConverter<Amount>
{
    public override Amount Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options) =>
            new Amount(reader.GetDecimal());

    public override void Write(
        Utf8JsonWriter writer,
        Amount amount,
        JsonSerializerOptions options) =>
            writer.WriteNumberValue(amount.Value);
}

public sealed class CurrencyJsonConverter : JsonConverter<Currency>
{
    public override Currency Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options) =>
            new Currency(reader.GetString());

    public override void Write(
        Utf8JsonWriter writer,
        Currency currency,
        JsonSerializerOptions options) =>
            writer.WriteStringValue(currency.Value);
}

The converters can be used by adding them to the JsonSerializerOptions that are passed to the JsonSerializer.Serialize method.

using System.Text.Json;

var oneEur = new Money(new Amount(1), new Currency("EUR"));
var options = new JsonSerializerOptions
{
    WriteIndented = true,
    Converters = {
        new AmountJsonConverter(),
        new CurrencyJsonConverter()
    }
};
var json = JsonSerializer.Serialize(oneEur, options);

Now the produced JSON is no longer unnecessary nested.

{
  "Amount": 1,
  "Currency": "EUR"
}

Another way to reach the same goal would be to implement a JSON converter for Money instead of implementing converters for Amount and Currency. In this converter, an intermediate type can be used for serialization and deserialization. This is especially useful when extensive customizations are necessary. A converter for Money could look like this:

public sealed class MoneyJsonConverter : JsonConverter<Money>
{
    private sealed record JsonMoney(decimal Amount, string Currency);

    public override Money Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    {
        var jm = JsonSerializer.Deserialize<JsonMoney>(ref reader, options);
        return new Money(new Amount(jm.Amount), new Currency(jm.Currency));
    }

    public override void Write(
        Utf8JsonWriter writer,
        Money money,
        JsonSerializerOptions options)
    {
        var jm = new JsonMoney(money.Amount.Value, money.Currency.Value);
        JsonSerializer.Serialize(writer, jm, options);
    }
}

Custom JSON converters are a powerful tool to finely control the serialization and deserialization of complex data structures. I often use them when I have to conform to a specific JSON format while still being able to use expressive data structures in the code.