1

I've been adopting the new System.Text.Json library in all my new projects. I understand how to use it, and it seems pretty similar to the Newtonsoft api.

The JsonConverter class in Newtonsoft was non-generic, and one converter could handle multiple types, so it made sense that every time you read a value, you have to specify the type of the value you want to read. However, the JsonConverter<T> in the System.Text.Json namespace is generic, and the return value is strongly typed, so you should always know at compile time what type you want to convert. If you don't know the type at compile time, then you have to use a JsonConverterFactory instead.

Why does the Read method have the signature public T Read(reader, typeToConvert, options)? When would typeToConvert be more specific than T?

Andrew Williamson
  • 8,299
  • 3
  • 34
  • 62
  • Perhaps have a read at the source code for some of the built in converters and see how they use it? For example, [`JsonValueConverterKeyValuePair`](https://github.com/dotnet/runtime/blob/81bf79fd9aa75305e55abe2f7e9ef3f60624a3a1/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterKeyValuePair.cs). They seem to use it to get another converter, that can read the nested JSON values. – Sweeper Jul 19 '21 at 05:04
  • *When would typeToConvert be more specific than T?* - Why does `typeToConvert` have to have anything at all to do with `T`? – Caius Jard Jul 19 '21 at 05:51

1 Answers1

4

As can be seen from the reference source, JsonConverter<T>.CanConvert(Type typeToConvert) is not sealed:

public override bool CanConvert(Type typeToConvert)
{
    return typeToConvert == typeof(T);
}

Thus it is possible for applications to override this method and return true for some type assignable to T, e.g.:

public override bool CanConvert(Type typeToConvert) => typeof(T).IsAssignableFrom(typeToConvert);

In such a situation, in Read (ref reader, Type typeToConvert, options), typeToConvert will be the declared type of the value being deserialized while T is the type the converter can convert. You will need to use reflection (e.g. (T)Activator.CreateInstance(typeToConvert)) to construct the instance being deserialized.

To take a concrete example, imagine you are dealing with the situation from How to handle both a single item and an array for the same property using System.Text.Json? and need to deserialize a JSON value that is sometimes an array and sometimes a single item to a Collection<string>. To do this, you define the following JsonConverter<Collection<TValue>>:

public class SingleOrArrayCollectionConverter<TItem> : JsonConverter<Collection<TItem>>
{
    public SingleOrArrayCollectionConverter() : this(true) { }
    public SingleOrArrayCollectionConverter(bool canWrite) => CanWrite = canWrite;
    public bool CanWrite { get; }
    
    public override bool CanConvert(Type typeToConvert) => typeof(Collection<TItem>).IsAssignableFrom(typeToConvert);

    public override Collection<TItem> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var list = typeToConvert == typeof(Collection<TItem>) ? new Collection<TItem>() : (Collection<TItem>)Activator.CreateInstance(typeToConvert);
        switch (reader.TokenType)
        {
            case JsonTokenType.StartArray:
                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndArray)
                        break;
                    list.Add(JsonSerializer.Deserialize<TItem>(ref reader, options));
                }
                break;
            default:
                list.Add(JsonSerializer.Deserialize<TItem>(ref reader, options));
                break;
        }
        return list;
    }
    
    public override void Write(Utf8JsonWriter writer, Collection<TItem> value, JsonSerializerOptions options)
    {
        if (CanWrite && value.Count == 1)
            JsonSerializer.Serialize(writer, value.First(), options);
        else
        {
            writer.WriteStartArray();
            foreach (var item in value)
                JsonSerializer.Serialize(writer, item, options);
            writer.WriteEndArray();
        }
    }
}

Then the converter can be used to deserialize to any subclass of Collection<string> such as ObservableCollection<string>. If you have some JSON that might be a single string or an array of strings, you may do:

var json = @"""hello"""; // A JSON string primitive "hello"
var options = new JsonSerializerOptions
{
    Converters = { new SingleOrArrayCollectionConverter<string>() },
};
var collection = JsonSerializer.Deserialize<ObservableCollection<string>>(json, options);

Notes:

  • If you do override CanConvert, it is your responsibility to ensure that it only returns true for types assignable to T. In cases where the types are not compatible, System.Text.Json will throw an InvalidOperationException such as:

     System.InvalidOperationException: The converter 'TConverter' is not compatible with the type 'TActualValue'.
    
  • For type hierarchies that include open generics, you must use the factory pattern to create specific JsonConverter<T> instances for each concrete type to be deserialized. How to handle both a single item and an array for the same property using System.Text.Json? shows one such factory which generates a SingleOrArrayConverter<TItem> converter for all List<TItem> types.

    You may opt to use the factory pattern for type hierarchies with no open types as well. The factory pattern would, for instance, allow you to manufacture converters only for subtypes of some type T that have a public parameterless constructor (i.e. that satisfy the where T : new() constraint). But for class hierarchies with no open generics, the factory pattern is optional rather than required.

  • For an example of a JsonConverter<T> that overrides CanConvert and deserializes a polymorphic type hierarchy, see this answer to Is polymorphic deserialization possible in System.Text.Json? by ahsonkhan.

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Oh, I didn't realize you can still override the `CanConvert` method, I assumed a converter was chosen based on the generic parameter. Just to be clear, does the JsonSerializer attempt to cast the returned `T` value to the declared type `typeToConvert` if they are different? – Andrew Williamson Jul 19 '21 at 21:24
  • It's chosen by the generic parameter *by default* but you can override it at your own risk e.g. to support polymorphism. The framework does check to make sure `T` and `typeToConvert` are compatible. If not you get the InvalidOperationException exception I mentioned. – dbc Jul 19 '21 at 23:13