Deserialising JSON using JsonSerializer.DeserializeAsync is not using my JsonConverter

Viewed 7391

A server is returning a JSON string value which is a URL query string:

{
    "parameters": "key1=value1&key2=value2"
}

I have a property set up to receive this, and convert it into a Dictionary as part of the deserialisation process:

Property with JsonConverter attribute:

[JsonConverter(typeof(QueryStringToDictionaryJsonConverter))]
public Dictionary<string, string> Parameters { get; set; }

Converter:

public class QueryStringToDictionaryJsonConverter : JsonConverter<Dictionary<string, string>> {

    public override Dictionary<string, string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {

        var queryString = reader.GetString();
        if (string.IsNullOrEmpty(queryString)) return null;

        return QueryHelpers.ParseQuery(queryString).ToDictionary(e => e.Key, e => string.Join(",", e.Value.ToArray()));

    }

    ...
}

This should work.

But it's not even getting to my converter.

From what I can tell, JsonSerializer.DeserializeAsync<T>(myJson) is seeing that the type of property is a Dictionary, and so it tries to parse the value as such on its own, and fails (the resulting exception is an 'invalid cast' as it tries to GetEnumerable() etc). A breakpoint in my converter never even gets hit.

I can get it to work by making the property an object and then casting to a Dictionary later where it's used, but that's an ugly solution.

Is there a way to force JsonSerializer.DeserializeAsync<T>(myJson) to just use my converter, without it trying to be smart on its own?

(I'm using Microsoft's System.Text.Json in .NET Core 3)

2 Answers

OK, so this could be a bug in System.Text.Json.

Here's the workaround I'm currently using for anyone else needing a solution.

First, I set up two properties for deserialisation, using [JsonPropertyName] and [JsonIgnore]:

[JsonPropertyName("parameters"), JsonConverter(typeof(QueryStringToDictionaryJsonConverter))]
public object ParametersObject { get; set; }

[JsonIgnore]
public Dictionary<string, string> Parameters => ParametersObject as Dictionary<string, string>;

And then in the JsonConverter, I allow object as the type:

public override bool CanConvert(Type typeToConvert) {
    if (typeToConvert == typeof(object)) return true;
    return base.CanConvert(typeToConvert);
}

Consumers of my deserialised class just use the Parameters property, which will continue to work just fine if and when this bug is fixed and I change the class back to how I'd like it.

I would create a wrapper and create a converter for the wrapper.

[JsonConverter( typeof( QueryStringDictionaryConverter ) )]
class QueryStringDictionary : Dictionary<string,string> { }

class QueryStringDictionaryConverter : JsonConverter<QueryStringDictionary>
{
    ... 
}

class MyClass
{
    public QueryStringDictionary Parameters { get; set; }
}

Alternatively you could use JsonSerializerOptions

class MyOtherClass
{
   public Dictionary<string,string> Parameters { get; set; }
}
MyOtherClass Deserialize( string json )
{
    var options = new JsonSerializerOptions
    {
        Converters = { new QueryStringToDictionaryJsonConverter() }
    };
    return JsonSerializer.Deserialize<MyOtherClass>( json, options );  
} 

A potential problem with this approach is that the converter would be used on all Dictionary<string,string> properties, which may not be intended. It would work fine for the simple example in the original question.

Related