我正在尝试使用Json.NET
使用JsonConvert.PopulateObject
将JSON数据反序列化为现有层次结构。除了儿童收藏品,一切都很好。
我希望将目标集合项与反序列化项目同步,以便使用具有匹配键的源对象更新目标项目,添加不存在的目标对象,并删除不存在的源对象。
我如何以及在何处定制反序列化逻辑以实现此行为?
答案 0 :(得分:1)
基于Json.Net PopulateObject - update list elements based on ID的答案,这里有一对转换器,它们将现有列表与从JSON反序列化的列表同步,根据键属性添加,删除或填充列表项:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class JsonMergeKeyAttribute : System.Attribute
{
}
public abstract class KeyedListSynchronizingConverterBase : JsonConverter
{
protected static bool CanConvert(IContractResolver contractResolver, Type objectType, out Type elementType, out JsonProperty keyProperty)
{
if (objectType.IsArray)
{
// Not implemented for arrays, since they cannot be resized.
elementType = null;
keyProperty = null;
return false;
}
var elementTypes = objectType.GetIListItemTypes().ToList();
if (elementTypes.Count != 1)
{
elementType = null;
keyProperty = null;
return false;
}
elementType = elementTypes[0];
var contract = contractResolver.ResolveContract(elementType) as JsonObjectContract;
if (contract == null)
{
keyProperty = null;
return false;
}
keyProperty = contract.Properties.Where(p => p.AttributeProvider.GetAttributes(typeof(JsonMergeKeyAttribute), true).Count > 0).SingleOrDefault();
return keyProperty != null;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var contractResolver = serializer.ContractResolver;
Type elementType;
JsonProperty keyProperty;
if (!CanConvert(contractResolver, objectType, out elementType, out keyProperty))
throw new JsonSerializationException(string.Format("Invalid input type {0}", objectType));
if (elementType.IsValueType)
throw new NotImplementedException("Not implemented for value types");
if (reader.TokenType == JsonToken.Null)
return null;
var method = typeof(KeyedListSynchronizingConverterBase).GetMethod("ReadJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
var genericMethod = method.MakeGenericMethod(new[] { elementType });
try
{
return genericMethod.Invoke(this, new object[] { reader, objectType, existingValue, serializer, keyProperty });
}
catch (TargetInvocationException ex)
{
// Wrap the TargetInvocationException in a JsonSerializationException
throw new JsonSerializationException("ReadJsonGeneric<T> error", ex);
}
}
object ReadJsonGeneric<T>(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer, JsonProperty keyProperty) where T : class
{
var list = existingValue as IList<T>;
if (list == null || list.Count == 0)
{
var contractResolver = serializer.ContractResolver;
list = list ?? (IList<T>)contractResolver.ResolveContract(objectType).DefaultCreator();
serializer.Populate(reader, list);
}
else
{
var jArray = JArray.Load(reader);
var lookup = Enumerable.Range(0, list.Count)
.Where(i => list[i] != null)
.ToLookup(i => keyProperty.ValueProvider.GetValue(list[i]), i => KeyValuePair.Create(i, list[i]), EqualityComparer<object>.Default);
var done = new HashSet<int>(); // In case there are duplicate keys, pair them in order.
for (int i = 0, count = jArray.Count; i < count; i++)
{
T item;
if (jArray[i].Type == JTokenType.Null)
item = null;
else
{
var key = jArray[i][keyProperty.PropertyName].ToObject(keyProperty.PropertyType, serializer);
var pair = lookup[key].Where(p => !done.Contains(p.Key)).FirstOrDefault();
item = pair.Value;
if (item == null)
{
item = jArray[i].ToObject<T>(serializer);
}
else
{
using (var subReader = jArray[i].CreateReader())
serializer.Populate(subReader, item);
}
done.Add(pair.Key);
}
if (i < list.Count)
list[i] = item;
else
list.Add(item);
}
while (list.Count > jArray.Count)
list.RemoveAt(list.Count - 1);
}
return list;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public class KeyedListSynchronizingConverter : KeyedListSynchronizingConverterBase
{
readonly IContractResolver contractResolver;
public KeyedListSynchronizingConverter(IContractResolver contractResolver)
{
if (contractResolver == null)
throw new ArgumentNullException("contractResolver");
this.contractResolver = contractResolver;
}
public override bool CanConvert(Type objectType)
{
Type elementType;
JsonProperty keyProperty;
return CanConvert(contractResolver, objectType, out elementType, out keyProperty);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (contractResolver != serializer.ContractResolver)
throw new InvalidOperationException("Inconsistent contract resolvers");
return base.ReadJson(reader, objectType, existingValue, serializer);
}
}
public class KeyedListPropertySynchronizingConverter : KeyedListSynchronizingConverterBase
{
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException("This converter is intended to be applied to a specific property, rather than globally");
}
}
然后将key属性应用于您的集合项,如下所示,以指示用于合并的键:
public class Item
{
[JsonMergeKey]
public Guid Id { get; set; }
public string Value { get; set; }
}
然后它可以全局使用:
var contractResolver = JsonSerializer.CreateDefault().ContractResolver;
var settings = new JsonSerializerSettings { ContractResolver = contractResolver, Converters = new [] { new KeyedListSynchronizingConverter(contractResolver) } };
JsonConvert.PopulateObject(newJson, rootObject, settings);
(请注意,根对象不应该是要同步的IList<T>
,因为Json.NET在执行Populate()
时不会为根对象调用转换器。)
或者,您可以将其应用于特定的IList<T>
属性,如下所示:
public class RootObject
{
[JsonConverter(typeof(KeyedListPropertySynchronizingConverter))]
public ObservableCollection<Item> Items { get; set; } // Can be any type of collection implementing IList<T>
}