在反序列化具有来自 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);
}
}
我有一个具有 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 anIDictionary<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);
}
}