我正在尝试使用接口为Json.NET反序列化创建一个抽象层。
为此,我使用了自定义JsonConverter
,在引入接口之前,它的工作效果很好。
引发以下异常:
未处理的异常:Newtonsoft.Json.JsonSerializationException:错误 在“批次列表”上将值设置为“商品”。 -> 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; }
。
答案 0 :(得分:1)
您需要做的是结合以下两个转换器的功能:
SingleOrArrayConverter
从this answer到 How to handle both a single item and an array for the same property using JSON.net ,Brian Rogers。
此转换器处理经常遇到的情况,即单项集合未序列化为集合;您已经在使用此转换器。
ConcreteConverter<IInterface, TConcrete>
从this answer到 How to deserialize collection of interfaces when concrete classes contains other interfaces 。
此转换器将声明的接口(此处为IBatchItems
)反序列化为指定的具体类型(此处为BatchItems
)。这是必需的,因为IList<T>
不是covariant,因此无法像您当前尝试的那样将IList<BatchItems>
分配给IList<IBatchItems>
。
结合这两个转换器的最佳方法是采用decorator pattern并增强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
,该转换器根据存在的属性智能地选择要使用的具体类型,例如 Deserializing polymorphic json classes without type information using json.net 和 How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects? 的答案。
演示小提琴here。