ArgumentNullException in Json.NET 6.0.7 when deserializing in NameValueCollection

I wrote some custom JsonConverters to deserialize json text into System.Net.Mail.MailMessage objects. Here is the complete code that you can run in LINQPad. Interestingly, this code works as expected in 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(); } } 

The exception is:

 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) 

Is this a bug in JSON 6? Am I doing something wrong?

EDIT: through further debugging, I decided the problem was with the Headers property.

+4
json c #
source share
1 answer

The main problem is that MailMessage.Headers returns a NameValueCollection that looks like a dictionary but does not implement an IDictionary<TKey, TValue> or even not a generic IDictionary . Instead, it implements non-common ICollection and IEnumerable interfaces. In fact, these interfaces consist in scrolling only keys only completely ignoring the values.

Thus, if I create a NameValueCollection like this:

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

and serialize it with Json.NET 6.0.7, it sees that the incoming class is an nonequivalent collection and serializes it as an array :

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

production:

  ["Sam","Bill"] 

As you can see, the values ​​have been deleted.

Then, when deserializing, Json.NET tries to convert the string array back to NameValueCollection , but has no way to do this. In particular, he tries to create a temporary list for storing read data, but gets confused by the basic type of the list and throws an exception. This might be a bug in Json.NET, but even if it didn't throw an exception, the data was already lost during storage. This can be reproduced with a simple test class, for example:

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

So the question is do you want to read the headlines or do you want to ignore them? And if you want to read them, in what format will you receive them? If you want to send and receive them successfully, you will need to write a custom JsonConverter . This is a bit complicated, because the NameValueCollection almost like Dictionary<string, string []> , but it keeps the order of adding keys, which Dictionary not. Ideally, serialization should preserve this order. This can be done by creating and serializing the proxy IDictionary<string, string []> :

 public class NameValueCollectionDictionaryWrapper<TNameValueCollection> : IDictionary<string, string[]> where TNameValueCollection : NameValueCollection, new() { readonly TNameValueCollection collection; public NameValueCollectionDictionaryWrapper() : this(new TNameValueCollection()) { } public NameValueCollectionDictionaryWrapper(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); 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) { value = collection.GetValues(key); return value != null; } public ICollection<string[]> Values { get { return Enumerable.Range(0, collection.Count).Select(i => collection.GetValues(i)).ToArray(); } } public string[] this[string key] { get { var value = collection.GetValues(key); if (value == null) throw new KeyNotFoundException(); 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 } 

Then create the following JsonConverter :

 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.TokenType == JsonToken.Null) return null; NameValueCollectionDictionaryWrapper<TNameValueCollection> dictionaryWrapper; 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(); dictionaryWrapper = new NameValueCollectionDictionaryWrapper<TNameValueCollection>(); // EMPTY DICTIONARY } else { dictionaryWrapper = serializer.Deserialize<NameValueCollectionDictionaryWrapper<TNameValueCollection>>(reader); } var collection = (TNameValueCollection)existingValue; if (dictionaryWrapper == null) return collection; if (collection == null) collection = dictionaryWrapper.GetCollection(); else collection.Add(dictionaryWrapper.GetCollection()); return collection; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var collection = (TNameValueCollection)value; var dictionaryWrapper = new NameValueCollectionDictionaryWrapper<TNameValueCollection>(collection); serializer.Serialize(writer, dictionaryWrapper); } } 

Finally, apply NameValueJsonConverter<NameValueCollection> , like your other converters. This produces a Json-style result while maintaining order, for example:

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

I do not have Json.NET 4.x for testing, but I doubt that it correctly serializes both the keys and the NameValueCollection values. You might want to install this version to double check what it did.

Refresh

Just checked out Json.NET 4.5.11. In this version, the NameValueCollection property in my test class NameValueCollectionWrapper serialized as an array of key strings, which are then ignored during deserialization (collection returns empty). So this is probably a regression that version Json.NET version 6 throws an exception rather than ignoring the property.

+4
source share

All Articles