Search code examples
c#json.netjsonconvert

How to fix InvalidCastException while using JsonConverter for an IList of Interfaces?


I am trying to create an abstraction layer for Json.NET deserialization using interfaces. To achieve this I use custom JsonConverter which works just fine, until interfaces are introduced. Following exception is thrown:

Unhandled Exception: Newtonsoft.Json.JsonSerializationException: Error setting value to 'Items' on 'BatchList'. ---> System.InvalidCastException: Unable to cast object of type 'System.Collections.Generic.List1[BatchItems]' to type 'System.Collections.Generic.List`1[IBatchItems]

This is the setup to repro in a console app:

class Program
{
    static void Main(string[] args)
    {
        var jsonBatch = @"{'items': [{'Id': 'name1','info': {'age': '20'}},{'Id': 'name2','info': {'age': '21'}}]}";
        DeserializeAndPost(jsonBatch);
    }

    public static void DeserializeAndPost(string json)
    {
        IBatchList req;
        req = JsonConvert.DeserializeObject<BatchList>(json);
        Post(req);
    }

    public static void Post(IBatchList batchList)
    {
        Console.WriteLine(batchList.Items.FirstOrDefault().Id);
    }
}

public interface IBatchList
{
    List<IBatchItems> Items { get; set; }
}

public interface IBatchItems
{
    string Id { get; set; }
    JObject Info { get; set; }
}

[JsonObject(MemberSerialization.OptIn)]
public class BatchList : IBatchList
{
    [JsonProperty(PropertyName = "Items", Required = Required.Always)]
    [JsonConverter(typeof(SingleOrArrayConverter<BatchItems>))]
    public List<IBatchItems> Items { get; set; }

}

[JsonObject]
public class BatchItems : IBatchItems
{
    [JsonProperty(PropertyName = "Id", Required = Required.Always)]
    public string Id { get; set; }
    [JsonProperty(PropertyName = "Info", Required = Required.Always)]
    public JObject Info { get; set; }
}

// JsonConverter

public class SingleOrArrayConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(List<T>));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        if (token.Type == JTokenType.Array)
        {
            return token.ToObject<List<T>>();
        }
        return new List<T> { token.ToObject<T>() };
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        List<T> list = (List<T>)value;
        if (list.Count == 1)
        {
            value = list[0];
        }
        serializer.Serialize(writer, value);
    }

    public override bool CanWrite
    {
        get { return true; }
    }
}

I expect the output to be deserialized JSON as I provide the type for the interface to be used for deserialization:

 [JsonConverter(typeof(SingleOrArrayConverter<BatchItems>))]

to be used. Instead, unhandled cast exception is being thrown.

Note that if I use instead SingleOrArrayConverter<IBatchItems>, I will get an exception

Newtonsoft.Json.JsonSerializationException: Could not create an instance of type

as the [JsonConverter(typeof(SingleOrArrayConverter<BatchItems>))] is meant to provide concrete type for the following interface: public List<IBatchItems> Items { get; set; }.


Solution

  • What you need to do is to combine the functionality of the following two converters:

    1. SingleOrArrayConverter from this answer to How to handle both a single item and an array for the same property using JSON.net by Brian Rogers.

      This converter handles the frequently-encountered case where a one-item collection is not serialized as a collection; you are already using this converter.

    2. ConcreteConverter<IInterface, TConcrete> from this answer to How to deserialize collection of interfaces when concrete classes contains other interfaces.

      This converter deserializes a declared interface (here IBatchItems) into a specified concrete type (here BatchItems). This is required because IList<T> is not covariant and thus an IList<BatchItems> cannot be assigned to a IList<IBatchItems> as you are currently trying to do.

    The best way to combine these two converters is to adopt the decorator pattern and enhance SingleOrArrayConverter to encapsulate a converter for each of the list's items inside the list converter:

    public class SingleOrArrayListItemConverter<TItem> : JsonConverter
    {
        // Adapted from the answers to https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n
        // By Brian Rogers, dbc et. al.
    
        readonly JsonConverter itemConverter;
        readonly bool canWrite;
    
        public SingleOrArrayListItemConverter(Type itemConverterType) : this(itemConverterType, true) { }
    
        public SingleOrArrayListItemConverter(Type itemConverterType, bool canWrite)
        {
            this.itemConverter = (JsonConverter)Activator.CreateInstance(itemConverterType);
            this.canWrite = canWrite;
        }
    
        public override bool CanConvert(Type objectType)
        {
            return typeof(List<TItem>).IsAssignableFrom(objectType);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (reader.MoveToContent().TokenType == JsonToken.Null)
                return null;
            var contract = serializer.ContractResolver.ResolveContract(objectType);
            var list = (ICollection<TItem>)(existingValue ?? contract.DefaultCreator());
            if (reader.TokenType != JsonToken.StartArray)
            {
                list.Add(ReadItem(reader, serializer));
                return list;
            }
            else
            {
                while (reader.ReadToContent())
                {
                    switch (reader.TokenType)
                    {
                        case JsonToken.EndArray:
                            return list;
                        default:
                            list.Add(ReadItem(reader, serializer));
                            break;
                    }
                }
                // Should not come here.
                throw new JsonSerializationException("Unclosed array at path: " + reader.Path);
            }
        }
    
        TItem ReadItem(JsonReader reader, JsonSerializer serializer)
        {
            if (itemConverter.CanRead)
                return (TItem)itemConverter.ReadJson(reader, typeof(TItem), default(TItem), serializer);
            else
                return serializer.Deserialize<TItem>(reader);
        }
    
        public override bool CanWrite { get { return canWrite; } }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var list = value as ICollection<TItem>;
            if (list == null)
                throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
            if (list.Count == 1)
            {
                foreach (var item in list)
                    WriteItem(writer, item, serializer);
            }
            else
            {
                writer.WriteStartArray();
                foreach (var item in list)
                    WriteItem(writer, item, serializer);
                writer.WriteEndArray();
            }
        }
    
        void WriteItem(JsonWriter writer, TItem value, JsonSerializer serializer)
        {
            if (itemConverter.CanWrite)
                itemConverter.WriteJson(writer, value, serializer);
            else
                serializer.Serialize(writer, value);
        }
    }
    
    public class ConcreteConverter<IInterface, TConcrete> : JsonConverter where TConcrete : IInterface
    {
        //Taken from the answer to https://stackoverflow.com/questions/47939878/how-to-deserialize-collection-of-interfaces-when-concrete-classes-contains-other
        // by dbc
        public override bool CanConvert(Type objectType)
        {
            return typeof(IInterface) == objectType;
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            return serializer.Deserialize<TConcrete>(reader);
        }
    
        public override bool CanWrite { get { return false; } }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    
    public static partial class JsonExtensions
    {
        public static JsonReader MoveToContent(this JsonReader reader)
        {
            if (reader.TokenType == JsonToken.None)
                reader.Read();
            while (reader.TokenType == JsonToken.Comment && reader.Read())
                ;
            return reader;
        }
    
        public static bool ReadToContent(this JsonReader reader)
        {
            if (!reader.Read())
                return false;
            while (reader.TokenType == JsonToken.Comment)
                if (!reader.Read())
                    return false;
            return true;
        }
    }
    

    Then apply it as follows:

    [JsonObject(MemberSerialization.OptIn)]
    public class BatchList : IBatchList
    {
        [JsonProperty(PropertyName = "Items", Required = Required.Always)]
        [JsonConverter(typeof(SingleOrArrayListItemConverter<IBatchItems>), typeof(ConcreteConverter<IBatchItems, BatchItems>))]
        public List<IBatchItems> Items { get; set; }
    }
    

    Notes:

    Demo fiddle here.