如何将具有键属性同步的对象反序列化为现有集合?

时间:2016-01-20 08:14:30

标签: c# json json.net

我正在尝试使用Json.NET使用JsonConvert.PopulateObject将JSON数据反序列化为现有层次结构。除了儿童收藏品,一切都很好。

我希望将目标集合项与反序列化项目同步,以便使用具有匹配键的源对象更新目标项目,添加不存在的目标对象,并删除不存在的源对象。

我如何以及在何处定制反序列化逻辑以实现此行为?

1 个答案:

答案 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>
}