反序列化为NameValueCollection时,Json.NET 6.0.7中的ArgumentNullException

我写了一些自定义JsonConverters来将json文本反序列化为System.Net.Mail.MailMessage对象。 这是完整的代码,可以在LINQPad中运行。 有趣的是,此代码在Json.NET 4.5.11中按预期运行:

 void Main() { const string JsonMessage = @"{ ""From"": { ""Address"": ""askywalker@theEmpire.gov"", ""DisplayName"": ""Darth Vader"" }, ""Sender"": null, ""ReplyTo"": null, ""ReplyToList"": [], ""To"": [ { ""Address"": ""lskywalker@theRebellion.org"", ""DisplayName"": ""Luke Skywalker"" } ], ""Bcc"": [], ""CC"": [ { ""Address"": ""lorgana@alderaan.gov"", ""DisplayName"": ""Princess Leia"" } ], ""Priority"": 0, ""DeliveryNotificationOptions"": 0, ""Subject"": ""Family tree"", ""SubjectEncoding"": null, ""Headers"": [], ""HeadersEncoding"": null, ""Body"": ""I am your father!"", ""BodyEncoding"": ""US-ASCII"", ""BodyTransferEncoding"": -1, ""IsBodyHtml"": true, ""Attachments"": [ { ""FileName"": ""skywalker family tree.jpg"", ""ContentBase64"": ""AQIDBAU="" } ], ""AlternateViews"": [] }"; JsonConvert.DeserializeObject(JsonMessage, new MailAddressReadConverter(), new AttachmentReadConverter(), new EncodingReadConverter()).Dump(); } public class MailAddressReadConverter : JsonConverter { public override bool CanConvert(Type objectType) { return objectType == typeof(MailAddress); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var messageJObject = serializer.Deserialize(reader); if (messageJObject == null) { return null; } var address = messageJObject.GetValue("Address", StringComparison.OrdinalIgnoreCase).ToObject(); JToken displayNameToken; string displayName; if (messageJObject.TryGetValue("DisplayName", StringComparison.OrdinalIgnoreCase, out displayNameToken) && !string.IsNullOrEmpty(displayName = displayNameToken.ToObject())) { return new MailAddress(address, displayName); } return new MailAddress(address); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } } public class AttachmentReadConverter : JsonConverter { public override bool CanConvert(Type objectType) { return objectType == typeof(Attachment); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var info = serializer.Deserialize(reader); var attachment = info != null ? new Attachment(new MemoryStream(Convert.FromBase64String(info.ContentBase64)), "application/octet-stream") { ContentDisposition = { FileName = info.FileName } } : null; return attachment; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } private class AttachmentInfo { [JsonProperty(Required = Required.Always)] public string FileName { get; set; } [JsonProperty(Required = Required.Always)] public string ContentBase64 { get; set; } } } public class EncodingReadConverter : JsonConverter { public override bool CanConvert(Type objectType) { return typeof(Encoding).IsAssignableFrom(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var encodingName = serializer.Deserialize(reader); return encodingName.NullSafe(s => Encoding.GetEncoding(s)); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } } 

例外情况是:

 System.ArgumentNullException : Value cannot be null. at System.RuntimeType.MakeGenericType(Type[] instantiation) at Newtonsoft.Json.Serialization.JsonArrayContract.CreateWrapper(Object list) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateList(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, Object existingValue, String id) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, Object target) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent) at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType) at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings) at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonConverter[] converters) at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, JsonConverter[] converters) 

这是JSON 6中的错误吗? 难道我做错了什么?

编辑:通过进一步调试,我已经确定问题是Headers属性。

这里的基本问题是MailMessage.Headers返回一个NameValueCollection ,它有点像字典,但没有实现IDictionary甚至非genericsIDictionary 。 相反,它实现了非通用接口ICollectionIEnumerable 。 这些接口实际上做的是仅遍历集合的 ,完全忽略值。

因此,如果我像这样创建一个NameValueCollection

  public static NameValueCollection CreateCollection() { NameValueCollection collection = new NameValueCollection(); FillCollection(collection); return collection; } private static void FillCollection(NameValueCollection collection) { collection.Add("Sam", "Dot Net Perls"); collection.Add("Bill", "Microsoft"); collection.Add("Bill", "White House"); collection.Add("Sam", "IBM"); } 

并使用Json.NET 6.0.7对其进行序列化,它看到传入的类是一个非generics集合并将其序列化为一个数组 :

  var collection = CreateCollection(); var json = JsonConvert.SerializeObject(collection); Debug.WriteLine(json); 

生产:

  ["Sam","Bill"] 

如您所见,值已被剥离。

然后在反序列化时,Json.NET尝试将字符串数组转换回NameValueCollection ,但无法这样做。 特别是,它尝试构造一个临时列表来保存正在读取的数据,但是对列表的基本类型感到困惑,并抛出exception。 这可能是Json.NET中的一个错误,但即使它没有抛出exception,数据也已经在存储上丢失了。 这可以通过如下简单的测试类重现:

 public class NameValueCollectionWrapper { public NameValueCollectionWrapper() { this.Collection = new NameValueCollection(); } public NameValueCollection Collection { get; private set; } } 

所以,问题是,你想阅读标题,还是想忽略它们? 如果你想阅读它们,你将以什么格式接收它们? 如果要成功发送和接收它们,则需要编写自定义JsonConverter 。 这样做有点棘手,因为NameValueCollection 几乎就像是Dictionary但是它保留了添加键的顺序 ,而Dictionary却没有。 理想情况下,序列化应该保留该顺序。 这可以通过创建和序列化适配器模式包装器IDictionary例如从这个答案到如何将NameValueCollection转换为JSON字符串的那个

 public class NameValueCollectionDictionaryAdapter : IDictionary where TNameValueCollection : NameValueCollection, new() { readonly TNameValueCollection collection; public NameValueCollectionDictionaryAdapter() : this(new TNameValueCollection()) { } public NameValueCollectionDictionaryAdapter(TNameValueCollection collection) { this.collection = collection; } // Method instead of a property to guarantee that nobody tries to serialize it. public TNameValueCollection GetCollection() { return collection; } #region IDictionary Members public void Add(string key, string[] value) { if (collection.GetValues(key) != null) throw new ArgumentException("Duplicate key " + key); if (value == null) collection.Add(key, null); else foreach (var str in value) collection.Add(key, str); } public bool ContainsKey(string key) { return collection.GetValues(key) != null; } public ICollection Keys { get { return collection.AllKeys; } } public bool Remove(string key) { bool found = ContainsKey(key); if (found) collection.Remove(key); return found; } public bool TryGetValue(string key, out string[] value) { return (value = collection.GetValues(key)) != null; } public ICollection Values { get { return new ReadOnlyCollectionAdapter, string[]>(this, p => p.Value); } } public string[] this[string key] { get { var value = collection.GetValues(key); if (value == null) throw new KeyNotFoundException(key); return value; } set { Remove(key); Add(key, value); } } #endregion #region ICollection> Members public void Add(KeyValuePair item) { Add(item.Key, item.Value); } public void Clear() { collection.Clear(); } public bool Contains(KeyValuePair item) { string[] value; if (!TryGetValue(item.Key, out value)) return false; return EqualityComparer.Default.Equals(item.Value, value); // Consistent with Dictionary } public void CopyTo(KeyValuePair[] array, int arrayIndex) { foreach (var item in this) array[arrayIndex++] = item; } public int Count { get { return collection.Count; } } public bool IsReadOnly { get { return false; } } public bool Remove(KeyValuePair item) { if (Contains(item)) return Remove(item.Key); return false; } #endregion #region IEnumerable> Members public IEnumerator> GetEnumerator() { foreach (string key in collection) yield return new KeyValuePair(key, collection.GetValues(key)); } #endregion #region IEnumerable Members System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion } public static class NameValueCollectionExtensions { public static NameValueCollectionDictionaryAdapter ToDictionaryAdapter(this TNameValueCollection collection) where TNameValueCollection : NameValueCollection, new() { if (collection == null) throw new ArgumentNullException(); return new NameValueCollectionDictionaryAdapter(collection); } } public class ReadOnlyCollectionAdapter : CollectionAdapterBase> { public ReadOnlyCollectionAdapter(ICollection collection, Func toOuter) : base(() => collection, toOuter) { } public override void Add(TOut item) { throw new NotImplementedException(); } public override void Clear() { throw new NotImplementedException(); } public override bool IsReadOnly { get { return true; } } public override bool Remove(TOut item) { throw new NotImplementedException(); } } public abstract class CollectionAdapterBase : ICollection where TCollection : ICollection { readonly Func getCollection; readonly Func toOuter; public CollectionAdapterBase(Func getCollection, Func toOuter) { if (getCollection == null || toOuter == null) throw new ArgumentNullException(); this.getCollection = getCollection; this.toOuter = toOuter; } protected TCollection Collection { get { return getCollection(); } } protected TOut ToOuter(TIn inner) { return toOuter(inner); } #region ICollection Members public abstract void Add(TOut item); public abstract void Clear(); public virtual bool Contains(TOut item) { var comparer = EqualityComparer.Default; foreach (var member in Collection) if (comparer.Equals(item, ToOuter(member))) return true; return false; } public void CopyTo(TOut[] array, int arrayIndex) { foreach (var item in this) array[arrayIndex++] = item; } public int Count { get { return Collection.Count; } } public abstract bool IsReadOnly { get; } public abstract bool Remove(TOut item); #endregion #region IEnumerable Members public IEnumerator GetEnumerator() { foreach (var item in Collection) yield return ToOuter(item); } #endregion #region IEnumerable Members IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion } 

接下来,创建以下JsonConverter ,它既可以序列化和反序列化NameValueCollection ,也可以使用破旧的格式跳过值:

 public class NameValueJsonConverter : JsonConverter where TNameValueCollection : NameValueCollection, new() { public override bool CanConvert(Type objectType) { return typeof(TNameValueCollection).IsAssignableFrom(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.SkipComments().TokenType == JsonToken.Null) return null; var collection = (TNameValueCollection)existingValue ?? new TNameValueCollection(); var dictionaryWrapper = collection.ToDictionaryAdapter(); if (reader.TokenType != JsonToken.StartObject) { // Old buggy name value collection format in which the values were not written and so cannot be recovered. // Skip the token and all its children. reader.Skip(); } else { serializer.Populate(reader, dictionaryWrapper); } return collection; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var collection = (TNameValueCollection)value; var dictionaryWrapper = new NameValueCollectionDictionaryAdapter(collection); serializer.Serialize(writer, dictionaryWrapper); } } public static partial class JsonExtensions { public static JsonReader SkipComments(this JsonReader reader) { while (reader.TokenType == JsonToken.Comment && reader.Read()) ; return reader; } } 

最后,在执行其他转换器时应用NameValueJsonConverter 。 这会在保留顺序的同时以Json字典样式生成输出,例如:

 {"Sam":["Dot Net Perls","IBM"],"Bill":["Microsoft","White House"]} 

我没有Json.NET 4.x可供测试,但我怀疑它是否正确序列化了NameValueCollection的键和值。 您可能希望安装该版本以重复检查它的function。

更新

刚刚查看了Json.NET 4.5.11。 在该版本中,我的NameValueCollectionWrapper测试类中的NameValueCollection属性被序列化为一个键字符串数组,然后在反序列化时忽略 (该集合返回空)。 因此,Json.NET版本6抛出exception而不是忽略该属性可能是一种回归。