如何在使用 JsonConverter 进行接口 IList 时修复 InvalidCastException?



我正在尝试使用接口创建一个用于 Json.NET 反序列化的抽象层。 为了实现这一点,我使用自定义JsonConverter,在引入接口之前,它工作得很好。 引发以下异常:

未处理的异常:Newtonsoft.Json.JsonSerializationException:Error 在"批处理列表"上将值设置为"项目"。--->系统无效强制转换异常:无法强制转换类型的对象 "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. SingleOrArrayConverter这个答案 如何使用 Brian Rogers 的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; }
}

笔记:

  • 此版本的SingleOrArrayListItemConverter<TItem>避免将整个阵列预加载到JToken层次结构中,这可能会提高性能。

  • 如果IBatchItems以后变成多态的,您可以将ConcreteConverter替换为转换器,该转换器根据存在的属性智能地选择要使用的具体类型,如json.net 反序列化没有类型信息的多态 json类的答案和如何在 JSON.NET 中实现自定义 JsonConverter 以反序列化基类对象列表?

演示小提琴在这里。

相关内容

最新更新