Search code examples
c#jsonjson.netjsonconverter

How to apply a custom JsonConverter to the values inside a list inside a dictionary?


I have a CustomConverter : JsonConverter<int> for integers, and I need to add a [JsonConverter(typeof(CustomConverter))] attribute to a Dictionary<string, List<int>> property. Applying the custom converter to an int, List or Dictionary works fine:

public class Example 
{
    [JsonConverter(typeof(CustomConverter))]
    public int ExampleInt { get; set; }
    [JsonProperty(ItemConverterType = typeof(CustomConverter))]
    public List<int> ExampleList { get; set; }
    
    // How do I specify the Converter attribute for the int in the following line?
    public Dictionary<string, List<int>> ExampleDictionary { get; set; }
}

However I can't figure out how to specify that the CustomConverter should be used for the int values inside the List inside the Dictionary. How can I do this?


Solution

  • Dictionary<string, List<int>> is a nested collection of collections, and you are looking for something like ItemOfItemsConverterType, corresponding to ItemConverterType, to specify a converter for the items of the items of the collection. Unfortunately, no such attribute is implemented. Instead, it will be necessary to create a converter for the nested List<int> collection that calls the required innermost item converter.

    This can be done by implementing the following JsonConverter decorator for List<>:

    public class ListItemConverterDecorator : JsonConverter
    {
        readonly JsonConverter itemConverter;
        
        public ListItemConverterDecorator(Type type) => 
            itemConverter = (JsonConverter)Activator.CreateInstance(type ?? throw new ArgumentNullException());
    
        public override bool CanConvert(Type objectType) =>
            !objectType.IsPrimitive && objectType != typeof(string) && objectType.BaseTypesAndSelf().Any(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(List<>));
        
        public override bool CanRead => itemConverter.CanRead;
        public override bool CanWrite => itemConverter.CanWrite;
        
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var itemType = objectType.BaseTypesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(List<>)).Select(t => t.GetGenericArguments()[0]).First();
            if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
                return null;
            if (reader.TokenType != JsonToken.StartArray)
                throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, JsonToken.StartArray));
            var list = existingValue as IList ?? (IList)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
            while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndArray)
                list.Add(itemConverter.ReadJson(reader, itemType, null, serializer));
            return list;
        }
        
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            writer.WriteStartArray();
            foreach (var item in (IList)value)
                if (item == null)
                    writer.WriteNull();
                else
                    itemConverter.WriteJson(writer, item, serializer);
            writer.WriteEndArray();
        }
    }
    
    public static partial class JsonExtensions
    {
        public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
            reader.ReadAndAssert().MoveToContentAndAssert();
    
        public static JsonReader MoveToContentAndAssert(this JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException();
            if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
                reader.ReadAndAssert();
            while (reader.TokenType == JsonToken.Comment) // Skip past comments.
                reader.ReadAndAssert();
            return reader;
        }
    
        public static JsonReader ReadAndAssert(this JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException();
            if (!reader.Read())
                throw new JsonReaderException("Unexpected end of JSON stream.");
            return reader;
        }
    }
    
    public static class TypeExtensions
    {
        public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
        {
            while (type != null)
            {
                yield return type;
                type = type.BaseType;
            }
        }
    }
    

    Then annotate your Example class as follows, using JsonPropertyAttribute.ItemConverterParameters to specify the inner item converter CustomConverter:

    public class Example 
    {
        [JsonConverter(typeof(CustomConverter))]
        public int ExampleInt { get; set; }
        [JsonProperty(ItemConverterType = typeof(CustomConverter))]
        public List<int> ExampleList { get; set; }
        
        [JsonProperty(ItemConverterType = typeof(ListItemConverterDecorator), 
                      ItemConverterParameters = new object [] { typeof(CustomConverter) })]
        public Dictionary<string, List<int>> ExampleDictionary { get; set; }
    }
    

    And now you should be all set. Demo fiddle here.