C# – Create a custom JsonConverter for System.Text.Json

Most of the time System.Text.Json will get you want you want. You can pass in options to control JSON serialization and deserialization to a certain extent. But sometimes you’ll run into scenarios where you need to customize how it handles a specific type.

This is where JsonConverter comes in. You can customize serialization / deserialization for a specific type by implementing JsonConverter<T>. This has two methods: Write() for serializing, Read() for deserializing.

public class NullableDateTimeConverter : JsonConverter<DateTime?>
{
	public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		//deserialize JSON into a DateTime?
		return null;
	}

	public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
	{
		//serialize a DateTime? object
	}
}
Code language: C# (cs)

To use this custom converter, add it to JsonSerializarOptions.Converters. Then pass in options to JsonSerializer, like this:

var message = new Message()
{
	Text = "Executed PayCustomer command",
	SentAt = DateTime.UtcNow
};

var options = new JsonSerializerOptions();
options.Converters.Add(new NullableDateTimeConverter());

var json = JsonSerializer.Serialize(message, options);
Code language: C# (cs)

JsonSerializer will use NullableDateTimeConverter to handle nullable DateTime properties.

In this article, I’ll show how to implement the JsonConverter.Read()/Write() methods.

Keep it simple – make your JsonConverter very specific

You’ve heard the saying – “Don’t reinvent the wheel.” This applies here: try to leverage JsonSerializer as much possible and only create your own converter when you have no other options.

With that said, when you create your own converter, try to be very specific by following these guidelines:

  • Make your converter handle a very specific type.
  • Only pass in the converter to JsonSerializer when you will definitely need it.
  • If you only need it for serialization, only implement JsonConvert.Write().
  • If you only need it for deserialization, only implement JsonConvert.Read().

Let’s see how these guidelines would be applied to a real world scenario. Let’s say I have the following LogMessage class:

public class LogMessage
{
	public string Text { get; set; }
	public Exception Exception { get; set; }
}
Code language: C# (cs)

When serializing this, I want to convert the Exception property to a string containing Exception.Message. To do this, I’d create the following custom converter:

public class LogMessageExceptionJsonConverter : JsonConverter<Exception>
{
	public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		//Not used in deserialization
		return null;
	}
	public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options)
	{
		writer.WriteStringValue(value.Message);
	}
}
Code language: C# (cs)

Next, I would only use this converter when serializing LogMessage objects:

var options = new JsonSerializerOptions();
options.Converters.Add(new LogMessageExceptionJsonConverter());

var json = JsonSerializer.Serialize(logMessage, options);
Code language: C# (cs)

This meets the “be very specific” guidelines:

  • It only handles the Exception type.
  • Only Write() is implemented (because it’s only being used for serialization).
  • The custom converter is only being added to JsonSerializerOptions.Converters when it’s needed.

By being very specific, you avoid surprises and can keep the JsonConverter logic as simple as possible.

How to implement JsonConverter.Write()

In JsonConverter.Write() you use Utf8JsonWriter to serialize the passed in object.

JSON has three types of properties: values, objects, and arrays. I’ll show how to use Utf8JsonWriter to write each type of property.

I have the following Message object that I want to serialize:

var message = new Message()
{
	From = new Person()
	{
		Name = "Albert Einstein"
	},
	To = new Person()
	{
		Name = "Isaac Newton"
	},
	SentAt = new DateTime(year: 1687, month: 7, day: 4),
	WinningNumbers = new List<int>()
	{
		1, 2, 3, 5, 8, 13
	}
};
Code language: C# (cs)

I would write the JsonConverter like this:

public override void Write(Utf8JsonWriter writer, Message value, JsonSerializerOptions options)
{
	writer.WriteStartObject();

	writer.WriteString(nameof(Message.SentAt), value.SentAt.ToString("M/d/yyyy"));

	writer.WriteStartObject(nameof(Message.To));
	writer.WriteString(nameof(Person.Name), value.To?.Name);
	writer.WriteEndObject();

	writer.WriteStartObject(nameof(Message.From));
	writer.WriteString(nameof(Person.Name), value.From?.Name);
	writer.WriteEndObject();

	writer.WriteStartArray(nameof(Message.WinningNumbers));
	value.WinningNumbers?.ForEach(line => writer.WriteNumberValue(line));
	writer.WriteEndArray();

	writer.WriteEndObject();
}
Code language: C# (cs)

This generates the following JSON:

{
	"SentAt": "7/4/1687",
	"To": {
		"Name": "Isaac Newton"
	},
	"From": {
		"Name": "Albert Einstein"
	},
	"WinningNumbers": [1,2,3,5,8,13]
}
Code language: JSON / JSON with Comments (json)

Next, I’ll show examples of writing JSON values, objects, and arrays.

Write a JSON value

You can write strings, numbers, and bools like this:

writer.WriteString("Date", DateTime.Now);
writer.WriteNumber("Number", 1);
writer.WriteBoolean("Bool", true);
Code language: C# (cs)

This generates the following JSON:

{
  "Date": "2021-01-16T10:16:03.719736-05:00",
  "Number": 1,
  "Bool": true
}
Code language: JSON / JSON with Comments (json)

Write a JSON object

To write a JSON object:

  • Start the object with writer.WriteStartObject().
  • Write values/arrays/objects to the object.
  • End the object with writer.WriteEndObject().

Here’s an example:

writer.WriteStartObject();
writer.WriteString("Message", "Hello world");
writer.WriteEndObject();
Code language: C# (cs)

This outputs the following JSON:

{
  "Message": "Hello world"
}
Code language: JSON / JSON with Comments (json)

If the object you’re writing is contained in another JSON object, specify the object name like this:

writer.WriteStartObject("Message");
writer.WriteString("Text", "Hello world");
writer.WriteEndObject();
Code language: C# (cs)

This outputs the following:

"Message": {
  "Text": "Hello world"
}
Code language: JSON / JSON with Comments (json)

Write a JSON array

To write a JSON array:

  • Start the array with writer.WriteStartArray().
  • Write objects/values/arrays to the array.
  • End the array with writer. WriteEndArray().

Here’s an example:

int[] numbers = new int[] { 1, 2, 3 };
writer.WriteStartArray();
foreach(var n in numbers)
{
	writer.WriteNumberValue(n);
}
writer.WriteEndArray();
Code language: C# (cs)

This generates the following JSON array:

[1,2,3]Code language: JSON / JSON with Comments (json)

How to implement JsonConverter.Read()

In the Read() method, you use Utf8JsonReader to read and deserialize JSON:

  • Loop through the JSON by calling reader.Read().
  • Check reader.TokenType to figure out what you’re dealing with.
  • When you’re in a value token (like a string), you use reader.GetString() to get the value.

Here’s an example of looping through JSON and outputting the token types. This is a good first step when you’re trying to figure out how to read and deserialize JSON:

public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
	Console.WriteLine($"TokenType={reader.TokenType}");
	while (reader.Read())
	{
		switch (reader.TokenType)
		{
			case JsonTokenType.StartObject:
			case JsonTokenType.EndObject:
			case JsonTokenType.StartArray:
			case JsonTokenType.EndArray:
				Console.WriteLine($"TokenType={reader.TokenType}");
				break;
			case JsonTokenType.String:
				Console.WriteLine($"TokenType=String Value={reader.GetString()}");
				break;
			case JsonTokenType.Number:
				Console.WriteLine($"TokenType=Number Value={reader.GetInt32()}");
				break;
			case JsonTokenType.PropertyName:
				Console.WriteLine($"TokenType=PropertyName Value={reader.GetString()}");
				break;
		}
	}
	return null;
}
Code language: C# (cs)

Note: Sometimes you may want to use JsonDocument for reading JSON instead.

When I run this again the Message JSON:

{
	"SentAt": "7/4/1687",
	"To": {
		"Name": "Isaac Newton"
	},
	"From": {
		"Name": "Albert Einstein"
	},
	"WinningNumbers": [1,2,3,5,8,13]
}
Code language: JSON / JSON with Comments (json)

It outputs the following list of tokens and values:

TokenType=StartObject
TokenType=PropertyName Value=SentAt
TokenType=String Value=7/4/1687
TokenType=PropertyName Value=To
TokenType=StartObject
TokenType=PropertyName Value=Name
TokenType=String Value=Isaac Newton
TokenType=EndObject
TokenType=PropertyName Value=From
TokenType=StartObject
TokenType=PropertyName Value=Name
TokenType=String Value=Albert Einstein
TokenType=EndObject
TokenType=PropertyName Value=WinningNumbers
TokenType=StartArray
TokenType=Number Value=1
TokenType=Number Value=2
TokenType=Number Value=3
TokenType=Number Value=5
TokenType=Number Value=8
TokenType=Number Value=13
TokenType=EndArray
TokenType=EndObject
Code language: plaintext (plaintext)

Next, I’ll show more examples of reading JSON values, objects, and arrays.

Reading a single JSON value

Let’s say you’re deserializing JSON that has datetime strings in the US date format (month/day/year), like this:

{
  "Name": "Bob",
  "SentAt": "7/4/1687"
}
Code language: JSON / JSON with Comments (json)

In the Read() method, use reader.GetString() to get the single value and then convert it to a DateTime with the US culture info:

public class USDateConverter : JsonConverter<DateTime>
{
	public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		DateTime date = DateTime.Parse(reader.GetString(), CultureInfo.GetCultureInfo("en-US"));
		return date;
	}
	//Write() not shown
}
Code language: C# (cs)

Note: When the converter is for a simple type like DateTime, the reader starts at the value token. So you don’t need to call reader.Read().

Reading a JSON object

Let’s say you have the following JSON that represents a Message class:

{
  "SentAt": "2021-01-17T15:55:36.5153627Z",
  "Text": "hello world!",
  "Id": "00007"
}
Code language: JSON / JSON with Comments (json)

To deserialize this object in the Read() method, loop through the JSON tokens. When you see a PropertyName token, get the property name (with reader.GetString()). The next token will be the property’s value. Use reader.GetString() (or the appropriate getter for the type).

Here’s an example:

public override Message Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
	if (reader.TokenType != JsonTokenType.StartObject)
		throw new JsonException("Expected StartObject token");

	var message = new Message();
	
	while (reader.Read())
	{
		if (reader.TokenType == JsonTokenType.EndObject)
			return message;
		
		if (reader.TokenType != JsonTokenType.PropertyName)
			throw new JsonException("Expected PropertyName token");

		var propName = reader.GetString();
		reader.Read();

		switch(propName)
		{
			case nameof(Message.Id):
				message.Id = Int32.Parse(reader.GetString());
				break;
			case nameof(Message.SentAt):
				message.SentAt = reader.GetDateTime();
				break;
			case nameof(Message.Text):
				message.Text = reader.GetString();
				break;
		}
	}

	throw new JsonException("Expected EndObject token");
}
Code language: C# (cs)

Reading a JSON array

Let’s say you have the following JSON array and you want to deserialize it to a HashSet<string>:

[1,2,3,4,5]
Code language: JSON / JSON with Comments (json)

To read a JSON array, keep looping until you see the EndArray token. Use reader.GetString() (or the appropriate type) to read each value from the array.

Here’s an example of reading the JSON array and deserializing it to HashSet<string>:

public override HashSet<string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
	if (reader.TokenType != JsonTokenType.StartArray)
		throw new JsonException("Expected StartArray token");

	var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

	while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
	{
		set.Add(reader.GetString());
	}

	return set;

}
Code language: C# (cs)

Comments are closed.