在反序列化具有来自 JSON 的重复键的字典时,如何强制抛出异常?

How can I force an exception to be thrown when deserializing a dictionary with duplicated keys from JSON?

我有一个具有 Dictionary<string, string> 属性的数据模型,如下所示:

public class Model
{
    public string Name { get; set; }
    public Dictionary<string, string> Attributes { get; set; }
}

在极少数情况下,我收到 JSON Attributes 的重复 属性 名称,例如:

{
   "name":"Object Name",
   "attributes":{
      "key1":"adfadfd",
      "key1":"adfadfadf"
   }
}

我希望在这种情况下抛出异常,但是当我用 Json.NET 反序列化时没有错误,字典反而包含遇到的最后一个值。在这种情况下如何强制错误?


作为解决方法,我目前将属性声明为 key/value 对的列表:

    public List<KeyValuePair<string, string>> Attributes { get; set; 

这需要我按以下格式序列化属性:

"attributes": [
    {
        "key": "key1",
        "value": "adfadfd"
    },
    {
        "key": "key1",
        "value": "adfadfadf"
    }
]

然后我可以检测到重复项。但是,我更愿意使用更紧凑的 JSON 对象语法而不是 JSON 数组语法,并将属性声明为字典。

似乎,当从具有重复 属性 名称的 JSON 对象反序列化字典时,Json.NET(以及 System.Text.Json)会默默地用来自最后一个复制键的值。 (演示 here.) This is not entirely surprising, as JSON RFC 8259 states

When the names within an object are not unique, the behavior of software that receives such an object is unpredictable. Many implementations report the last name/value pair only...

既然你不想这样,你可以创建一个 custom JsonConverter,它会在 属性 名称重复时抛出错误:

public class NoDuplicateKeysDictionaryConverter<TValue> : NoDuplicateKeysDictionaryConverter<Dictionary<string, TValue>, TValue> 
{
}

public class NoDuplicateKeysDictionaryConverter<TDictionary, TValue> : JsonConverter<TDictionary> where TDictionary : IDictionary<string, TValue>
{
    public override TDictionary ReadJson(JsonReader reader, Type objectType, TDictionary existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return typeof(TDictionary).IsValueType && Nullable.GetUnderlyingType(typeof(TDictionary)) == null ? throw new JsonSerializationException("null value") : default;
        reader.AssertTokenType(JsonToken.StartObject);
        var dictionary = existingValue ?? (TDictionary)serializer.ContractResolver.ResolveContract(typeof(TDictionary)).DefaultCreator();
        // Todo: decide whether you want to clear the incoming dictionary.
        while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
        {
            var key = (string)reader.AssertTokenType(JsonToken.PropertyName).Value;
            var value = serializer.Deserialize<TValue>(reader.ReadToContentAndAssert());
            // https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.idictionary-2.add#exceptions
            // Add() will throw an ArgumentException when an element with the same key already exists in the IDictionary<TKey,TValue>.
            dictionary.Add(key, value);
        }
        return dictionary;
    }

    public override bool CanWrite => false;
    public override void WriteJson(JsonWriter writer, TDictionary value, JsonSerializer serializer) => throw new NotImplementedException();
}

public static partial class JsonExtensions
{
    public static JsonReader AssertTokenType(this JsonReader reader, JsonToken tokenType) => 
        reader.TokenType == tokenType ? reader : throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, tokenType));
    
    public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
        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;
    }
}

然后将其添加到您的模型中,如下所示:

    [Newtonsoft.Json.JsonConverter(typeof(NoDuplicateKeysDictionaryConverter<string>))]
    public Dictionary<string, string> Attributes { get; set; }

每当尝试向字典中添加重复的键时,都会抛出 ArgumentException

演示文件 here

针对 Json.NET 查看 the source code,代码就是这样做的:

dictionary[keyValue] = itemValue;

因此,一种选择是为 Dictionary 编写一个包装器,以提供您想要的功能。我们可以传递所有调用,除了索引器,它传递给 Add 而不是会导致异常。

Techinically speaking, the Json.NET code only asks for an IDictionary, not an IDictionary<TKey, TValue> but then you wouldn't be able to read from it without casting and/or unboxing.

     const string json =@"
     {
        ""name"":""Object Name"",
        ""attributes"":{
           ""key1"":""adfadfd"",
           ""key1"":""adfadfadf""
        }
     }
     ";
     Console.WriteLine(JsonConvert.DeserializeObject<Model>(json));
 public class Model
 {
     public string Name { get; set; }
     public StrictDictionary<string, string> Attributes { get; set; }
 }
 
 public class StrictDictionary<TKey, TValue> : IDictionary<TKey, TValue>
 {
     public Dictionary<TKey, TValue> InnerDictionary {get; set; } = new Dictionary<TKey, TValue>();
     
     public bool ContainsKey(TKey key) => InnerDictionary.ContainsKey(key);
     public void Add(TKey key, TValue value) => InnerDictionary.Add(key, value);
     void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> kvp) => ((ICollection<KeyValuePair<TKey, TValue>>) InnerDictionary).Add(kvp);
     bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> kvp) => ((ICollection<KeyValuePair<TKey, TValue>>) InnerDictionary).Contains(kvp);
     void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array, int i) => ((ICollection<KeyValuePair<TKey, TValue>>) InnerDictionary).CopyTo(array, i);
     public void Clear() => InnerDictionary.Clear();
     public bool Remove(TKey key) => InnerDictionary.Remove(key);
     bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> kvp) => ((ICollection<KeyValuePair<TKey, TValue>>) InnerDictionary).Remove(kvp);
     public bool TryGetValue(TKey key, out TValue value) => InnerDictionary.TryGetValue(key, out value);
     public ICollection<TKey> Keys => InnerDictionary.Keys;
     public ICollection<TValue> Values => InnerDictionary.Values;
     public int Count => InnerDictionary.Count;
     public bool IsReadOnly => ((ICollection<KeyValuePair<TKey, TValue>>) InnerDictionary).IsReadOnly;
     public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => InnerDictionary.GetEnumerator();
     IEnumerator IEnumerable.GetEnumerator() => InnerDictionary.GetEnumerator();

     public TValue this[TKey key]
     {
         get => InnerDictionary[key];
         set => InnerDictionary.Add(key, value);
     }
 }

dotnetfiddle