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.