【问题标题】:Json.net - How to preserve dictionary value references when populating a dictionary?Json.net - 填充字典时如何保留字典值引用?
【发布时间】:2020-10-28 05:02:34
【问题描述】:

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

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

为 Newtonsoft.Json.JsonSerializer。注意不能引用 当通过非默认构造函数设置值时保留,例如 实现 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 中且我不想丢失的值:

[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;
}

有没有办法让它工作?

【问题讨论】:

  • 你可以序列化/反序列化to_serialize.Values而不是整个字典,不是吗?
  • @Sajid 我不明白你的评论
  • 好的,我想说,问题出在dictionary,因为它继承自ISerializable,为什么不只使用字典中的Values 进行序列化/反序列化,它是一个@ 987654329@ 类型所以将保留参考。
  • 您可能误解了PreserveReferencesHandling 的作用。 当使用PreserveReferencesHandling 设置进行往返时,它会保留序列化图中的引用。参见例如dotnetfiddle.net/tZ7fuD。它不保留对预先存在的对象的引用,并且在 JSON 存在时不起作用,除非PreserveReferencesHandling 用于序列化和反序列化。
  • 在您的示例中,您断言,在序列化往返之后,反序列化的字典值实际上包含对预先存在的模型的引用。这不会发生。 (哎呀,它可能在反序列化调用之前已被垃圾收集。)您认为它适用于列表的原因是JsonConvert.PopulateObject 追加到列表中,原始条目仍然存在。但是对于字典JsonConvert.PopulateObject 添加新键并覆盖现有键的值。演示小提琴#2:dotnetfiddle.net/1G9pf3

标签: c# json serialization json.net json-deserialization


【解决方案1】:

您的问题与 JsonSerializer.CreateDefault().Populate(..) resets my values 中的问题类似:您想填充预先存在的集合,特别是对于某些 TDictionary&lt;int, T&gt;,并填充预先存在的值。不幸的是,在字典的情况下,Json.NET 将 替换 值而不是填充它们,正如 JsonSerializerInternalReader.PopulateDictionary() 中所见,它只是将值反序列化为适当的类型,并将其设置为字典。

要解决此限制,您可以为Dictionary&lt;TKey, TValue&gt; 创建一个custom JsonConverter,当TKey 是原始类型而TValue 是复杂类型时,它将传入的JSON 键/值对合并到预先存在的字典中。以下转换器可以解决问题:

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()

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));
            }
        }
    }
}

您现在可以按如下方式填充您的字典:

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

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

注意事项:

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

  • 在您的问题中,您写了 // works ok with list&lt;Model&gt; 但实际上这是不正确的。当 List&lt;T&gt; 被填充时,新值将追加到列表中,因此 Assert.AreSame(to_serialize[0], model); 完全靠运气通过。如果你另外声明了Assert.AreSame(1, to_serialize.Count),它就会失败。

  • 虽然转换器适用于 stringint 等原始键,但它可能不适用于需要特定 JSON 转换的键类型,例如 enumDateTime

  • 转换器目前仅针对Dictionary&lt;TKey, TValue&gt; 实现,并利用了这种类型实现非泛型IDictionary 接口这一事实。如果需要,它可以扩展到其他字典类型,例如 SortedDictionary&lt;TKey,TValue&gt;

演示小提琴here.

【讨论】:

  • 非常感谢,经过一天半的时间试图弄清楚如何序列化我的状态并遇到默认序列化程序的限制问题,然后是 JSON .net 序列化程序也有限制
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多