【问题标题】:ArgumentNullException in Json.NET 6.0.7 when deserializing into NameValueCollection反序列化为 NameValueCollection 时 Json.NET 6.0.7 中的 ArgumentNullException
【发布时间】:2017-04-30 20:46:11
【问题描述】:

我编写了一些自定义 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"": ""<strong>I am your father!</strong>"",
  ""BodyEncoding"": ""US-ASCII"",
  ""BodyTransferEncoding"": -1,
  ""IsBodyHtml"": true,
  ""Attachments"": [
    {
      ""FileName"": ""skywalker family tree.jpg"",
      ""ContentBase64"": ""AQIDBAU=""
    }
  ],
  ""AlternateViews"": []
}";
    JsonConvert.DeserializeObject<MailMessage>(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<JObject>(reader);
            if (messageJObject == null)
            {
                return null;
            }

            var address = messageJObject.GetValue("Address", StringComparison.OrdinalIgnoreCase).ToObject<string>();

            JToken displayNameToken;
            string displayName;
            if (messageJObject.TryGetValue("DisplayName", StringComparison.OrdinalIgnoreCase, out displayNameToken)
                && !string.IsNullOrEmpty(displayName = displayNameToken.ToObject<string>()))
            {
                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<AttachmentInfo>(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<string>(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 属性上。

【问题讨论】:

    标签: c# json json.net


    【解决方案1】:

    这里的基本问题是MailMessage.Headers 返回一个NameValueCollection,它有点像字典,但没有实现IDictionary&lt;TKey, TValue&gt;,甚至没有实现非泛型IDictionary。相反,它实现了非通用接口ICollectionIEnumerable。这些接口实际上所做的只是循环遍历集合的keys,完全忽略这些值。

    因此,如果我 createNameValueCollection 像这样:

        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 对其进行序列化,它看到传入的类是一个非泛型集合和serializes it as an array

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

    制作:

            ["Sam","Bill"]
    

    如您所见,这些值已被剥离。

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

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

    所以,问题是,你想阅读标题,还是想忽略它们?如果你想阅读它们,你会以什么格式收到它们?如果要成功发送和接收它们,则需要编写自定义JsonConverter。这样做有点棘手,因为NameValueCollection几乎类似于Dictionary&lt;string, string []&gt;但它保留了添加键的顺序Dictionary 没有。理想情况下,序列化应该保持这个顺序。这可以通过创建和序列化 adapter pattern 包装器 IDictionary&lt;string, string []&gt; 来实现,例如从 this answerhow to convert NameValueCollection to JSON string? 的包装器:

    public class NameValueCollectionDictionaryAdapter<TNameValueCollection> : IDictionary<string, string[]>
        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<string,string[]> 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<string> 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<string[]> Values
        {
            get
            {
                return new ReadOnlyCollectionAdapter<KeyValuePair<string, string[]>, 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<KeyValuePair<string,string[]>> Members
    
        public void Add(KeyValuePair<string, string[]> item) { Add(item.Key, item.Value); }
    
        public void Clear() { collection.Clear(); }
    
        public bool Contains(KeyValuePair<string, string[]> item)
        {
            string[] value;
            if (!TryGetValue(item.Key, out value))
                return false;
            return EqualityComparer<string[]>.Default.Equals(item.Value, value); // Consistent with Dictionary<TKey, TValue>
        }
    
        public void CopyTo(KeyValuePair<string, string[]>[] 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<string, string[]> item)
        {
            if (Contains(item))
                return Remove(item.Key);
            return false;
        }
    
        #endregion
    
        #region IEnumerable<KeyValuePair<string,string[]>> Members
    
        public IEnumerator<KeyValuePair<string, string[]>> GetEnumerator()
        {
            foreach (string key in collection)
                yield return new KeyValuePair<string, string[]>(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<TNameValueCollection> ToDictionaryAdapter<TNameValueCollection>(this TNameValueCollection collection)
            where TNameValueCollection : NameValueCollection, new()
        {
            if (collection == null)
                throw new ArgumentNullException();
            return new NameValueCollectionDictionaryAdapter<TNameValueCollection>(collection);
        }
    }
    
    public class ReadOnlyCollectionAdapter<TIn, TOut> : CollectionAdapterBase<TIn, TOut, ICollection<TIn>>
    {
        public ReadOnlyCollectionAdapter(ICollection<TIn> collection, Func<TIn, TOut> 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<TIn, TOut, TCollection> : ICollection<TOut> 
        where TCollection : ICollection<TIn>
    {
        readonly Func<TCollection> getCollection;
        readonly Func<TIn, TOut> toOuter;
    
        public CollectionAdapterBase(Func<TCollection> getCollection, Func<TIn, TOut> 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<TOut> Members
    
        public abstract void Add(TOut item);
    
        public abstract void Clear();
    
        public virtual bool Contains(TOut item)
        {
            var comparer = EqualityComparer<TOut>.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<TOut> Members
    
        public IEnumerator<TOut> 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<TNameValueCollection> : 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<TNameValueCollection>(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&lt;NameValueCollection&gt;。这会在保留顺序的同时以 Json 字典样式生成输出,例如:

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

    我没有可用于测试的 Json.NET 4.x,但我怀疑它是否正确序列化了 NameValueCollection 的键和值。您可能需要安装该版本以仔细检查它的功能。

    更新

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-04-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多