c# - 反序列化为 NameValueCollection 时 Json.NET 6.0.7 中的 ArgumentNullException

标签 c# json json.net

我已经编写了一些自定义 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 属性上。

最佳答案

这里的基本问题是 MailMessage.Headers 返回 NameValueCollection ,有点像字典,但没有实现 IDictionary<TKey, TValue>甚至是非通用的 IDictionary .相反,它实现了非通用接口(interface) ICollectionIEnumerable .这些接口(interface)实际上做的是循环遍历 keys仅收集,完全忽略值。

因此,如果我 create一个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 对其进行序列化,它看到传入的类是一个非泛型集合并且 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<string, string []> , 但它保留了添加键的顺序,这 Dictionary 才不是。理想情况下,序列化应保留该顺序。这可以通过创建和序列化一个 adapter pattern 来完成。 wrapper IDictionary<string, string []>比如来自 this answer 的那个给 how 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<NameValueCollection>就像你做其他转换器一样。这会在保留顺序的同时生成 Json 字典样式的输出,例如:

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

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

更新

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

关于c# - 反序列化为 NameValueCollection 时 Json.NET 6.0.7 中的 ArgumentNullException,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27828350/

相关文章:

c# - 当使用 JSON 中的重复键反序列化字典时,如何强制抛出异常?

c# - 如何使用 json.net 设置 json 路径的值

c# - JsonConvert.DeserializeObject 在一个项目中工作,但在另一个项目中不起作用

c# - 动态类型列表

c# - 交易订单簿更新和计算

c# - 使用匿名对象初始化字典

javascript - jQuery UI 自动完成 : How to base on JSON data NOT name & Label

java - 使用 jackson 从字符串反序列化 LocalDateTime

c# - 服务已停止,但进程仍保持一分钟

asp.net - 页面的 JSON 输出