简体   繁体   中英

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

I have a data model with a Dictionary<string, string> of attributes as follows:

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

Under certain rare circumstances, I am receiving JSON with duplicated property names for Attributes , eg:

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

I would like for an exception to be thrown in such a situation, however when I deserialize with Json.NET there is no error and the dictionary instead contains the last value encountered. How can I force an error in such a situation?


As a workaround, I am currently declaring attributes as a list of key/value pairs:

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

This requires me to serialize the attributes in the following format:

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

Then later I can detect the duplicate. However, I would prefer to use the more compact JSON object syntax rather than the JSON array syntax, and declare Attributes as a dictionary.

It seems that, when deserializing a dictionary from a JSON object with duplicated property names, Json.NET (and also System.Text.Json) silently populate the dictionary with the value from the last duplicated key. (Demo 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...

Since you don't want that, you can create a custom JsonConverter that throws an error in the event of duplicated property names:

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://learn.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;
    }
}

Then add it to your model as follows:

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

And an ArgumentException will be thrown whenever an attempt is made to add duplicated keys to the dictionary.

Demo fidde here .

Looking at the source code for Json.NET, the code just does this:

dictionary[keyValue] = itemValue;

So one option is to write a wrapper for Dictionary that provides the functionality you want. We can pass through all calls, except for the indexer, which passes through to Add instead which will cause an exception.

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

do.netfiddle

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM