Json.net-填充字典时如何保留字典值引用? [英] Json.net - How to preserve dictionary value references when populating a dictionary?

查看:78
本文介绍了Json.net-填充字典时如何保留字典值引用?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想从JSON文件填充Dictionary中包含的对象,同时保留对象引用本身.

I would like to populate the objects contained within a Dictionary from a JSON file while preserving the object references themselves.

关于PreserveReferencesHandling的Json.net文档明确指出,如果类型实现System.Runtime.Serialization.ISerializable,则它将不起作用:

Json.net documentation on PreserveReferencesHandling clearly state that it will not work in case a type implements System.Runtime.Serialization.ISerializable:

为以下选项指定参考处理选项:Newtonsoft.Json.JsonSerializer.请注意,引用不能是通过非默认构造函数(例如)设置值时保留实现System.Runtime.Serialization.ISerializable的类型.

Specifies reference handling options for the Newtonsoft.Json.JsonSerializer. Note that references cannot be preserved when a value is set via a non-default constructor such as types that implement System.Runtime.Serialization.ISerializable.

这是我失败的代码:

class Model
{
   public int Val { get; set; } = 123;
}

...

    var model = new Model();
    var to_serialize = new Dictionary<int, Model> { { 0, model } }; // works ok with list<Model>

    // serialize
    var jsonString = JsonConvert.SerializeObject(to_serialize, Formatting.Indented);

    var jsonSerializerSettings = new JsonSerializerSettings();
    jsonSerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
    jsonSerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.All; // does not work for ISerializable
    
    Assert.AreSame(to_serialize[0], model); // ok!

    JsonConvert.PopulateObject(
        value: jsonString,
        target: to_serialize,
        settings: jsonSerializerSettings
    );

    Assert.AreSame(to_serialize[0], model); // not ok... works ok with list<Model>

我的主要要求是,当调用PopulateObject()时,将不会调用Model类的构造函数.取而代之的是,仅其内部字段将使用JSON中的值进行更新.在我的真实情况下,Model类包含其他不在JSON中并且我不想丢失的值:

My main requirement is that when calling PopulateObject(), the constructor of the Model class will not be invoked. Instead, only its internal field will be updated with the value from the JSON. In my real case, the Model class contains other values which are not in the JSON and which I don't want to lose:

[JsonObject(MemberSerialization.OptIn)]
class Model
{
   [JsonProperty(PropertyName = "val_prop")]
   public int Val { get; set; } = 123;

   // not in the json file, would like this field to maintain the value
   // it had prior to PopulateObject()
   public int OtherVal { get; set; } = 456;
}

有没有办法使这项工作成功?

Is there a way to make this work?

推荐答案

您的问题类似于 JsonSerializer中的问题.CreateDefault().Populate(..)重置我的值 :您想填充一个预先存在的集合,特别是某些 Dictionary< int,T> T ,然后填充先前存在的值.不幸的是,对于字典,Json.NET会替换这些值,而不是填充它们,如在

Your problem is similar to the one from JsonSerializer.CreateDefault().Populate(..) resets my values: you would like to populate a preexisting collection, specifically a Dictionary<int, T> for some T, and populate the preexisting values. Unfortunately, in the case of a dictionary, Json.NET will replace the values rather than populate them, as can be seen in JsonSerializerInternalReader.PopulateDictionary() which simply deserializes the value to the appropriate type, and sets it the dictionary.

要解决此限制,您可以创建一个自定义 JsonConverter 用于 Dictionary< TKey,TValue> ,当 TKey 是原始类型,而 TValue 是复杂类型并合并时传入的JSON键/值对到预先存在的字典中.以下转换器可以解决问题:

To work around this limitation, you can create a custom JsonConverter for Dictionary<TKey, TValue> when TKey is a primitive type and TValue is a complex type which merges the incoming JSON key/value pairs onto the preexisting dictionary. The following converter does the trick:

public class DictionaryMergeConverter : JsonConverter
{
    static readonly IContractResolver defaultResolver = JsonSerializer.CreateDefault().ContractResolver;
    readonly IContractResolver resolver = defaultResolver;

    public override bool CanConvert(Type objectType)
    {
        var keyValueTypes = objectType.GetDictionaryKeyValueType();
        if (keyValueTypes == null)
            return false;
        var keyContract = resolver.ResolveContract(keyValueTypes[0]);
        if (!(keyContract is JsonPrimitiveContract))
            return false;
        var contract = resolver.ResolveContract(keyValueTypes[1]);
        return contract is JsonContainerContract;
        // Also possibly check whether keyValueTypes[1] is a read-only collection or dictionary.
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return null;
        if (reader.TokenType != JsonToken.StartObject)
            throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
        IDictionary dictionary = existingValue as IDictionary ?? (IDictionary)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
        var keyValueTypes = objectType.GetDictionaryKeyValueType();
        while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
        {
            switch (reader.TokenType)
            {
                case JsonToken.PropertyName:
                    var name = (string)reader.Value;
                    reader.ReadToContentAndAssert();

                    // TODO: DateTime keys and enums with overridden names.
                    var key = (keyValueTypes[0] == typeof(string) ? (object)name : Convert.ChangeType(name, keyValueTypes[0], serializer.Culture));
                    var value = dictionary.Contains(key) ? dictionary[key] : null;

                    // TODO:
                    //  - JsonConverter active for valueType, either in contract or in serializer.Converters
                    //  - NullValueHandling, ObjectCreationHandling, PreserveReferencesHandling, 

                    if (value == null)
                    {
                        value = serializer.Deserialize(reader, keyValueTypes[1]);
                    }
                    else
                    {
                        serializer.Populate(reader, value);
                    }
                    dictionary[key] = value;
                    break;

                default:
                    throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
            }
        }

        return dictionary;
    }

    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 ReadToContentAndAssert(this JsonReader reader)
    {
        return reader.ReadAndAssert().MoveToContentAndAssert();
    }

    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

public static class TypeExtensions
{
    public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
    {
        while (type != null)
        {
            yield return type;
            type = type.BaseType;
        }
    }

    public static Type[] GetDictionaryKeyValueType(this Type type)
    {
        return type.BaseTypesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>)).Select(t => t.GetGenericArguments()).FirstOrDefault();
    }
}

这样做,您将遇到一个次要问题:Json.NET将永远不会使用自定义转换器来填充根对象.要解决此问题,您将需要调用 JsonConverter.ReadJson() 直接通过一些实用方法:

Having done so, you will encounter a secondary issue: Json.NET will never use a custom converter to populate the root object. To work around this you will need to call JsonConverter.ReadJson() directly, from some utility method:

public static partial class JsonExtensions
{
    public static void PopulateObjectWithConverter(string value, object target, JsonSerializerSettings settings)
    {
        if (target == null || value == null)
            throw new ArgumentNullException();
        var serializer = JsonSerializer.CreateDefault(settings);
        var converter = serializer.Converters.Where(c => c.CanConvert(target.GetType()) && c.CanRead).FirstOrDefault() ?? serializer.ContractResolver.ResolveContract(target.GetType()).Converter;
        using (var jsonReader = new JsonTextReader(new StringReader(value)))
        {
            if (converter == null)
                serializer.Populate(jsonReader, target);
            else
            {
                jsonReader.MoveToContentAndAssert();
                var newtarget = converter.ReadJson(jsonReader, target.GetType(), target, serializer);
                if (newtarget != target)
                    throw new JsonException(string.Format("Converter {0} allocated a new object rather than populating the existing object {1}.", converter, value));
            }
        }
    }
}

您现在可以按照以下步骤填充字典:

You will now be able to populate your dictionary as follows:

var jsonString = JsonConvert.SerializeObject(to_serialize, Formatting.Indented);

var settings = new JsonSerializerSettings
{
    Converters = { new DictionaryMergeConverter() },
};
JsonExtensions.PopulateObjectWithConverter(jsonString, to_serialize, settings);

注意:

  • PreserveReferencesHandling 对填充或替换字典值没有影响.相反,此设置控制具有多个对同一对象的引用的序列化图在往返时是否将保持其引用拓扑.

  • PreserveReferencesHandling has no impact on whether dictionary values are populated or replaced. Instead this setting controls whether a serialization graph with multiple references to the same object will maintain its reference topology when round-tripped.

在您的问题中,您编写的 //可以与list< Model> 一起使用,但实际上这是不正确的.当填充 List< T> 时,会将新值追加到列表中,因此 Assert.AreSame(to_serialize [0],model); 纯粹是靠运气.如果您另外声明了 Assert.AreSame(1,to_serialize.Count),它将失败.

In your question you wrote // works ok with list<Model> but in fact this is not correct. When a List<T> is populated the new values are appended to the list, so Assert.AreSame(to_serialize[0], model); passes purely by luck. If you had additionally asserted Assert.AreSame(1, to_serialize.Count) it would have failed.

虽然转换器将适用于诸如 string int 之类的原始键,但可能不适用于需要JSON特定转换的键类型(如>枚举 DateTime .

While the converter will work for primitive keys such as string and int it may not work for key types that require JSON-specific conversion such as enum or DateTime.

该转换器当前仅为 Dictionary< TKey,TValue> 实现,并利用了这种类型实现非通用 IDictionary 接口的事实.如果需要,它可以扩展到其他字典类型,例如 SortedDictionary< TKey,TValue> .

The converter is currently only implemented for Dictionary<TKey, TValue> and takes advantage of the fact that this type implements the non-generic IDictionary interface. It could be extended to other dictionary types such as SortedDictionary<TKey,TValue> if required.

演示小提琴此处.

这篇关于Json.net-填充字典时如何保留字典值引用?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆