C# Json.net-填充字典时如何保留字典值引用?
我想从JSON文件填充字典中包含的对象,同时保留对象引用本身 关于PreserveReferencesHandling的Json.net文档明确指出,如果某个类型实现System.Runtime.Serialization.ISerializable,它将不起作用: 指定对象的引用处理选项 Newtonsoft.Json.Json序列化程序。请注意,不能使用引用 通过非默认构造函数(例如)设置值时保留 实现System.Runtime.Serialization.ISerializable的类型 以下是我的失败代码:C# Json.net-填充字典时如何保留字典值引用?,c#,json,serialization,json.net,json-deserialization,C#,Json,Serialization,Json.net,Json Deserialization,我想从JSON文件填充字典中包含的对象,同时保留对象引用本身 关于PreserveReferencesHandling的Json.net文档明确指出,如果某个类型实现System.Runtime.Serialization.ISerializable,它将不起作用: 指定对象的引用处理选项 Newtonsoft.Json.Json序列化程序。请注意,不能使用引用 通过非默认构造函数(例如)设置值时保留 实现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>
有什么方法可以使这项工作正常进行吗?您的问题与中的问题类似:您希望填充一个预先存在的集合,特别是一些
T
的字典,并填充预先存在的值。不幸的是,在字典的情况下,Json.NET将替换值,而不是填充它们,从中可以看出,它只是将值反序列化为适当的类型,并将其设置为字典
要解决此限制,当TKey
是基本类型且TValue
是将传入的JSON键/值对合并到预先存在的字典上的复杂类型时,可以创建for字典。以下转换器实现了这一功能:
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();
}
}
您现在可以按如下方式填充词典:
var jsonString = JsonConvert.SerializeObject(to_serialize, Formatting.Indented);
var settings = new JsonSerializerSettings
{
Converters = { new DictionaryMergeConverter() },
};
JsonExtensions.PopulateObjectWithConverter(jsonString, to_serialize, settings);
注:
- 对是否填充或替换字典值没有影响。相反,此设置控制对同一对象具有多个引用的序列化图在往返时是否保持其引用拓扑
- 在您的问题中,您编写的
//可以与列表一起使用
,但事实上这是不正确的。填充列表时,新值将附加到列表中,因此断言.arame(要序列化[0],模型)代码>完全靠运气通过。如果您另外断言了Assert.arame(1,to_serialize.Count)
,那么它就会失败
- 虽然转换器适用于基本键,如
string
和int
,但它可能不适用于需要JSON特定转换的键类型,如enum
或DateTime
- 转换器目前仅为
字典
实现,并利用此类型实现非泛型IDictionary
接口这一事实。如果需要,它可以扩展到其他字典类型,如SortedDictionary
演示小提琴。您可以序列化/反序列化以进行序列化。值
而不是整个字典,否?@Sajid我不理解您的评论好的,我试图说,问题在于字典
,因为它继承自ISerializable
,为什么不使用字典的值
进行序列化/反序列化,它是值集合
类型,因此将保留引用。您可能误解了保留引用处理
的作用。当使用PreserveReferencesHandling
设置进行往返时,它将保留序列化图中的引用。见例。它不保留对预先存在的对象的引用,并且在JSON为时不起作用,除非PreserveReferencesHandling
用于序列化和反序列化。在您的示例中,您断言,在序列化往返之后,反序列化的字典值实际上包含对预先存在的模型的引用。这不会发生。(见鬼,它可能是在反序列化调用之前被垃圾收集的。)您认为它对列表有效的原因是JsonConvert.PopulateObject
附加到列表中,而原始条目仍然存在。但是对于字典JsonConvert.PopulateObject
添加新键并覆盖现有键的值。Demo fiddle#2在此:非常感谢您的支持,在一天半的时间里,我一直在试图弄清楚如何序列化我的状态,但遇到了默认序列化程序限制的问题,然后JSON.net序列化程序也有限制
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);