Как исправить InvalidCastException при использовании JsonConverter для IList интерфейсов? - PullRequest
1 голос
/ 11 апреля 2019

Я пытаюсь создать уровень абстракции для десериализации Json.NET с использованием интерфейсов.Чтобы добиться этого, я использую пользовательский JsonConverter, который прекрасно работает, пока не будут представлены интерфейсы.Выдается следующее исключение:

Необработанное исключение: Newtonsoft.Json.JsonSerializationException: Ошибка при установке значения «Items» в «BatchList».---> System.InvalidCastException: невозможно преобразовать объект типа 'System.Collections.Generic.List1 [BatchItems]' в тип 'System.Collections.Generic.List`1 [IBatchItems]

Это настройка для воспроизведения в консольном приложении:

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; }
    }
}

Я ожидаю, что выходные данные будут десериализованы в формате JSON, поскольку я предоставляю тип интерфейса, который будет использоваться для десериализации:

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

использоваться.Вместо этого выдается unhandled cast exception.

Обратите внимание, что если я использую вместо SingleOrArrayConverter<IBatchItems>, я получу исключение

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

, поскольку [JsonConverter(typeof(SingleOrArrayConverter<BatchItems>))] предназначен для предоставления конкретного типадля следующего интерфейса: public List<IBatchItems> Items { get; set; }.

1 Ответ

1 голос
/ 11 апреля 2019

Что вам нужно сделать, это объединить функциональность следующих двух преобразователей:

  1. SingleOrArrayConverter от этого ответа до Как обрабатывать как один элемент, так и массив для одного свойства с использованием JSON.net Брайан Роджерс .

    Этот конвертер обрабатывает часто встречающийся случай, когда коллекция из одного элемента не сериализуется как коллекция; Вы уже используете этот конвертер.

  2. ConcreteConverter<IInterface, TConcrete> с этот ответ до Как десериализовать коллекцию интерфейсов, когда конкретные классы содержат другие интерфейсы .

    Этот конвертер десериализует объявленный интерфейс (здесь IBatchItems) в указанный конкретный тип (здесь BatchItems). Это необходимо, потому что IList<T> не является ковариантным и, следовательно, IList<BatchItems> не может быть назначен на IList<IBatchItems>, как вы в настоящее время пытаетесь это сделать.

Лучший способ объединить эти два преобразователя - принять шаблон декоратора и улучшить SingleOrArrayConverter для инкапсуляции преобразователя для каждого элемента списка в преобразователе списка:

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;
    }
}

Затем примените его следующим образом:

[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; }
}

Примечания:

Демонстрационная скрипка здесь .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...