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 JsonCoverter 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 : JsonConverter<DateTime>
{
    public string Format { get; }

    public DateTimeConverter(string format)
    {
        Format = 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);
        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.

Let’s get to what a test looks like for this converter.

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.

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.

Cheers. Thanks for reading and sharing my blog posts with friends and colleagues.