System.Text.Json JsonConverter Test Helpers
Write reusable extension methods that simplify testing System.Text.Json JsonConverter Read and Write methods, catching edge cases without boilerplate.
A System.Text.Json JsonConverter<T> is a class that controls how a .NET type is read from and written to JSON. To unit test one, skip the serializer: call the converter’s Read and Write methods directly with a Utf8JsonReader and Utf8JsonWriter, and assert on the result. The extension methods below wrap that boilerplate into one-line calls.
Updated November 2026 — Code verified on .NET 10 with xUnit, and the converter now uses a C# 12 primary constructor.
In a perfect world, you’d have to write zero JsonConverter classes, as all JSON data would serialize and deserialize as expected. Unfortunately, we live in a world where folks make bespoke formatting decisions that can boggle the mind. For folks using System.Text.Json, you’ll likely have to write a JsonConverter to deal with these choices. When writing converters, you’ll want a test suite to ensure you’ve caught all the edge cases and to limit exceptions.
In this post, I’ll provide extension methods that make it a breeze to test any JsonConverter and a bonus class that makes it simpler to deal with double-quoting values.
The JSON Converter Example
Before we see the extension methods in action, let’s derive a JsonConverter definition.
using System.Globalization;using System.Text.Json;using System.Text.Json.Serialization;
namespace ConverterTests;
public class DateTimeConverter(string format) : JsonConverter<DateTime>{ public string Format { get; } = format;
public override DateTime Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) { var dateString = reader.GetString(); if (DateTime.TryParseExact( dateString, Format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result)) { return result; } }
throw new JsonException(); }
public override void Write( Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { var token = value.ToString(Format, CultureInfo.InvariantCulture); writer.WriteStringValue(token); }}Every JsonConverter has a Read and Write method. The read method allows you to process the appropriate token into the destination target. In this example, we’re taking a string with a particular date and time format and converting it into a DateTime instance. For the Write method, we use the same format to write the string value to the Utf8JsonWriter instance. Implementing either method depends on your use case and whether you’re serializing, deserializing, or performing both actions. The primary constructor (C# 12) keeps the format argument tidy.
Let’s get to what a test looks like for this converter.
How do I unit test a System.Text.Json JsonConverter?
Call the converter’s Read and Write methods directly instead of going through JsonSerializer. Using the extension methods from the next sections:
var converter = new DateTimeConverter("yyyy-MM-dd H:mm");
// Read: JSON token -> .NET valueDateTime date = converter.Read("\"2023-08-01 6:00\""); // 2023-08-01 06:00
// Write: .NET value -> JSON tokenstring json = converter.Write(new DateTime(2023, 8, 1, 6, 0, 0)); // "2023-08-01 6:00"
// Invalid input: assert on the exception, or use TryReadAssert.Throws<JsonException>(() => converter.Read("\"nope\""));Testing the converter directly gives you fast, focused tests with no serializer setup. Note that Read and Write here exercise only your converter; if you use source generation with a JsonSerializerContext, these helpers still test the converter in isolation, not the generated metadata.
Writing Tests for JSON Converters
Let’s take a look at the “ideal” test for executing the Read and Write methods. Note, these are extension methods, and the code is not calling the methods directly.
private readonly DateTimeConverter _sut = new("yyyy-MM-dd H:mm");
[Fact]public void Can_read_string_value_as_datetime(){ var result = _sut.Read("\"2023-08-01 6:00\""); Assert.Equal(new(2023, 8, 1, 6, 0, 0), result);}
[Fact]public void Can_write_datetime_as_string(){ var result = _sut.Write(new(2023, 8, 1, 6, 0, 0)); Assert.Equal("\"2023-08-01 6:00\"", result);}Oooo, so lovely. How did I accomplish such sweet tests? Well, it’s these extension methods.
using System.Text;using System.Text.Json;using System.Text.Json.Serialization;
namespace ConverterTests;
public static class JsonConverterTestExtensions{ public static TResult? Read<TResult>( this JsonConverter<TResult> converter, string token, JsonSerializerOptions? options = null) { options ??= JsonSerializerOptions.Default; var bytes = Encoding.UTF8.GetBytes(token); var reader = new Utf8JsonReader(bytes); // advance to token reader.Read(); var result = converter.Read(ref reader, typeof(TResult), options); // did we get the result? return result; }
public static (bool IsSuccessful, TResult? Result) TryRead<TResult>( this JsonConverter<TResult> converter, string token, JsonSerializerOptions? options = null) { try { var result = Read(converter, token, options); return (true, result); } catch (Exception) { return (IsSuccessful: false, Result: default); } }
public static string Write<T>( this JsonConverter<T> converter, T value, JsonSerializerOptions? options = null) { options ??= JsonSerializerOptions.Default; using var ms = new MemoryStream(); using var writer = new Utf8JsonWriter(ms); converter.Write(writer, value, options); writer.Flush(); var result = Encoding.UTF8.GetString(ms.ToArray()); return result; }
public static (bool IsSuccessful, string? Result) TryWrite<T>( this JsonConverter<T> converter, T value, JsonSerializerOptions? options = null) { try { var result = Write(converter, value, options); return (true, result); } catch { return (false, null); } }}These extensions have Read, TryRead, Write, and TryWrite methods to reduce the boilerplate code you might add to your tests.
Double Quoting JSON Tokens
You may have noticed in the tests above that tokens need to be double-quoted. This can be unpleasant, especially when you tweak values as you write more tests. That’s why I created a Quote class that utilized explicit and implicit cast operators to double-quote any value.
namespace ConverterTests;
public class Quote{ private readonly object _value;
private Quote(object value) => _value = value;
public static explicit operator Quote(string value) => new(value);
public static implicit operator string(Quote value) => value.ToString();
public override string ToString() => $"\"{_value}\"";}Let’s see it in action.
[Fact]public void Can_read_string_value_as_datetime(){ var result = _sut.Read((Quote)"2023-08-01 6:00"); Assert.Equal(new(2023, 8, 1, 6, 0, 0), result);}Now it should be easier to manage your values without escaping any double quotes.
FAQ
How do I test a JsonConverter’s Read and Write methods separately?
Write one test per direction. converter.Read(...) takes a JSON token string and returns the .NET value; converter.Write(...) takes the value and returns the JSON string. Add a TryRead test for invalid input so you can assert failure without try/catch in the test.
How do I pass JsonSerializerOptions to a converter in a test?
Every helper accepts an optional JsonSerializerOptions as its last argument. When omitted, JsonSerializerOptions.Default is used.
var options = new JsonSerializerOptions { WriteIndented = true };var json = _sut.Write(new(2023, 8, 1, 6, 0, 0), options);How do I test a converter registered with [JsonConverter] or Converters?
Those registrations are wired up by JsonSerializer, so use a round-trip test. A converter used in the [JsonConverter] attribute needs a parameterless constructor, so derive a small one:
public class IsoDateConverter() : DateTimeConverter("yyyy-MM-dd");
public class Order{ [JsonConverter(typeof(IsoDateConverter))] public DateTime Placed { get; set; }}
[Fact]public void Attribute_registered_converter_is_used(){ var json = JsonSerializer.Serialize(new Order { Placed = new(2023, 8, 1) }); Assert.Equal("{\"Placed\":\"2023-08-01\"}", json);}For JsonSerializerOptions.Converters, add new DateTimeConverter("yyyy-MM-dd H:mm") to the collection and call JsonSerializer.Serialize / Deserialize with those options.
Conclusion
The JsonConverter class is a necessary part of working with System.Text.Json and writing tests around your implementations is a must. I hope these extension methods make it easier for you and your team to maintain your implementations. If you need converters for more complex shapes, see serializing interface instances with System.Text.Json and polymorphic serialization with System.Text.Json, which pair well with these tests.
Cheers. Thanks for reading and sharing my blog posts with friends and colleagues.
Around the web
Comments
Loading comments from GitHub Discussions…