How to use JsonConverterFactory in C#

By FoxLearn 3/14/2025 3:45:51 AM   131
To use a JsonConverterFactory in C#, you need to create a custom JsonConverterFactory subclass and implement the necessary methods.

A JsonConverterFactory allows you to create multiple converters for different types based on some criteria. It's useful when you want to apply a custom converter to multiple types without writing separate converter classes for each type.

Imagine you want to serialize several DateTime types DateTime, DateTime?, DateTimeOffset, and DateTimeOffset? in a unified format. Specifically, you'd like to serialize them in the US date format, such as 3/14/2025.

There are two main ways to achieve this:

  1. Create a custom JsonConverter for each type.
  2. Use a JsonConverterFactory along with a generic custom JsonConverter.

How to implement and use a JsonConverterFactory in C#?

For example, How to implement a JsonConverterFactory that can handle DateTime, DateTime?, DateTimeOffset, and DateTimeOffset? types and serialize them to the US date format.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System.Text.Json;
using System.Text.Json.Serialization;
 
public class DateTimeConverterFactory : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert)
    {
        return typeToConvert == typeof(DateTime) ||
               typeToConvert == typeof(DateTime?) ||
               typeToConvert == typeof(DateTimeOffset) ||
               typeToConvert == typeof(DateTimeOffset?);
    }
 
    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        // Avoid caching converter objects here; JsonSerializer handles this for you.
        if (typeToConvert == typeof(DateTime))
        {
            return new DateTimeConverter<DateTime>();
        }
        else if (typeToConvert == typeof(DateTime?))
        {
            return new DateTimeConverter<DateTime?>();
        }
        else if (typeToConvert == typeof(DateTimeOffset))
        {
            return new DateTimeConverter<DateTimeOffset>();
        }
        else if (typeToConvert == typeof(DateTimeOffset?))
        {
            return new DateTimeConverter<DateTimeOffset?>();
        }
 
        throw new NotSupportedException("This type is not supported by the converter factory.");
    }
 
    // Generic nested converter for DateTime and DateTimeOffset types
    private class DateTimeConverter<T> : JsonConverter<T>
    {
        public override void Write(Utf8JsonWriter writer, T date, JsonSerializerOptions options)
        {
            writer.WriteStringValue((date as dynamic).ToString("MM/dd/yyyy")); // US date format
        }
 
        public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            // Deserialization is not necessary for this example.
            throw new NotImplementedException();
        }
    }
}

Use the JsonConverterFactory in Serialization

You can use the DateTimeConverterFactory in the JsonSerializerOptions to handle the serialization of your DateTime types:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var dates = new Dates()
{
    DateTime = DateTime.Now,
    DateTimeNullable = null,
    DateTimeOffset = DateTimeOffset.Now,
    DateTimeOffsetNullable = DateTimeOffset.Now
};
 
var options = new JsonSerializerOptions() { WriteIndented = true };
options.Converters.Add(new DateTimeConverterFactory());
 
var json = JsonSerializer.Serialize(dates, options);
 
Console.WriteLine(json);

This will produce the following JSON output, with all DateTime properties formatted in the US date style:

1
2
3
4
5
6
{
  "DateTime": "03/14/2025",
  "DateTimeNullable": null,
  "DateTimeOffset": "03/14/2025",
  "DateTimeOffsetNullable": "03/14/2025"
}

Why Use JsonConverterFactory?

The primary benefit of using the JsonConverterFactory is that it consolidates the logic for serializing multiple types into a single class. It simplifies the code and isolates the complexity of handling different DateTime types. The serializer code only needs to be aware of the JsonConverterFactory, not the individual converters.

Comparison with Multiple Custom Converters

An alternative to the JsonConverterFactory approach is to create separate custom converters for each type, as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class DateTimeConverter : JsonConverter<DateTime>
{
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
 
    public override void Write(Utf8JsonWriter writer, DateTime date, JsonSerializerOptions options)
    {
        writer.WriteStringValue(date.ToString("MM/dd/yyyy"));
    }
}
 
public class DateTimeNullableConverter : JsonConverter<DateTime?>
{
    public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
 
    public override void Write(Utf8JsonWriter writer, DateTime? date, JsonSerializerOptions options)
    {
        writer.WriteStringValue(date.GetValueOrDefault().ToString("MM/dd/yyyy"));
    }
}
 
// Additional converters for DateTimeOffset and DateTimeOffset? would be similar

Using this method, you'd need to add each converter individually to the serializer options:

1
2
3
4
5
6
7
8
var options = new JsonSerializerOptions() { WriteIndented = true };
options.Converters.Add(new DateTimeConverter());
options.Converters.Add(new DateTimeNullableConverter());
options.Converters.Add(new DateTimeOffsetConverter());
options.Converters.Add(new DateTimeOffsetNullableConverter());
 
var json = JsonSerializer.Serialize(dates, options);
Console.WriteLine(json);

Drawbacks of the Multiple Converter Approach:

  • Code Duplication: Each custom converter duplicates the logic for formatting the date, with only minor differences.
  • More Configuration: The client code must remember to add all the converters to the serializer options, which can be cumbersome.

In contrast, with the JsonConverterFactory approach, you only need to pass in a single converter, simplifying the code.

Why Keep the Generic Converter Private?

The generic DateTimeConverter class is marked as private to ensure it's only used for the specific DateTime types. This prevents misuse and potential runtime exceptions when the converter is applied to other types. The CanConvert() method restricts the types the factory can handle, making the use of dynamic safe in the serialization logic.

Dynamic Typing vs. Explicit Type Checking

While it's possible to use explicit type checking with if-else statements instead of dynamic, this would result in code duplication. Using dynamic allows the generic converter to remain clean and concise. However, be aware that dynamic typing introduces some performance overhead and the possibility of runtime exceptions if misused.

Why Not Use JsonConverter<object> or JsonConverter<dynamic>?

You might wonder if you could use a generic converter for object or dynamic. Unfortunately, this won't work with the JsonSerializer due to type mismatch errors:

1
2
public class ObjectConverter : JsonConverter<object> { ... }
public class DynamicConverter : JsonConverter<dynamic> { ... }

The serializer will throw an InvalidCastException when trying to use these converters with non-object or non-dynamic types.

In conclusion, the JsonConverterFactory approach is an elegant solution for serializing multiple DateTime types in a consistent format. It reduces complexity, eliminates redundant code, and simplifies the client code by requiring only a single converter.