Skip to content

Commit 8dbfb8e

Browse files
authored
Merge pull request #36424 from layomia/kvp_policy
Honor PropertyNamingPolicy, PropertyNameCaseInsensitive, & Encoder options when (de)serializing KeyValuePair instances
2 parents 77a3832 + ee45b87 commit 8dbfb8e

File tree

10 files changed

+515
-309
lines changed

10 files changed

+515
-309
lines changed

src/libraries/System.Text.Json/src/Resources/Strings.resx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,8 +396,8 @@
396396
<data name="SerializationConverterWrite" xml:space="preserve">
397397
<value>The converter '{0}' wrote too much or not enough.</value>
398398
</data>
399-
<data name="SerializerDictionaryKeyNull" xml:space="preserve">
400-
<value>The dictionary key policy '{0}' cannot return a null key.</value>
399+
<data name="NamingPolicyReturnNull" xml:space="preserve">
400+
<value>The naming policy '{0}' cannot return null.</value>
401401
</data>
402402
<data name="SerializationDuplicateAttribute" xml:space="preserve">
403403
<value>The attribute '{0}' cannot exist more than once on '{1}'.</value>

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ protected string GetKeyName(string key, ref WriteStack state, JsonSerializerOpti
4747

4848
if (key == null)
4949
{
50-
ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(options.DictionaryKeyPolicy.GetType());
50+
ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(options.DictionaryKeyPolicy);
5151
}
5252
}
5353

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/KeyValuePairConverter.cs

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,52 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System.Collections.Generic;
6+
using System.Text.Encodings.Web;
67

78
namespace System.Text.Json.Serialization.Converters
89
{
910
internal sealed class KeyValuePairConverter<TKey, TValue> : JsonValueConverter<KeyValuePair<TKey, TValue>>
1011
{
11-
private const string KeyName = "Key";
12-
private const string ValueName = "Value";
12+
private const string KeyNameCLR = "Key";
13+
private const string ValueNameCLR = "Value";
1314

14-
// todo: https://github.com/dotnet/runtime/issues/1197
15-
// move these to JsonSerializerOptions and use the proper encoding.
16-
private static readonly JsonEncodedText _keyName = JsonEncodedText.Encode(KeyName, encoder: null);
17-
private static readonly JsonEncodedText _valueName = JsonEncodedText.Encode(ValueName, encoder: null);
15+
// Property name for "Key" and "Value" with Options.PropertyNamingPolicy applied.
16+
private string _keyName = null!;
17+
private string _valueName = null!;
18+
19+
// _keyName and _valueName as JsonEncodedText.
20+
private JsonEncodedText _keyNameEncoded;
21+
private JsonEncodedText _valueNameEncoded;
1822

1923
// todo: https://github.com/dotnet/runtime/issues/32352
2024
// it is possible to cache the underlying converters since this is an internal converter and
2125
// an instance is created only once for each JsonSerializerOptions instance.
2226

27+
internal override void Initialize(JsonSerializerOptions options)
28+
{
29+
JsonNamingPolicy? namingPolicy = options.PropertyNamingPolicy;
30+
31+
if (namingPolicy == null)
32+
{
33+
_keyName = KeyNameCLR;
34+
_valueName = ValueNameCLR;
35+
}
36+
else
37+
{
38+
_keyName = namingPolicy.ConvertName(KeyNameCLR);
39+
_valueName = namingPolicy.ConvertName(ValueNameCLR);
40+
41+
if (_keyName == null || _valueName == null)
42+
{
43+
ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(namingPolicy);
44+
}
45+
}
46+
47+
JavaScriptEncoder? encoder = options.Encoder;
48+
_keyNameEncoded = JsonEncodedText.Encode(_keyName, encoder);
49+
_valueNameEncoded = JsonEncodedText.Encode(_valueName, encoder);
50+
}
51+
2352
internal override bool OnTryRead(
2453
ref Utf8JsonReader reader,
2554
Type typeToConvert, JsonSerializerOptions options,
@@ -44,17 +73,19 @@ internal override bool OnTryRead(
4473
ThrowHelper.ThrowJsonException();
4574
}
4675

76+
bool caseInsensitiveMatch = options.PropertyNameCaseInsensitive;
77+
4778
string propertyName = reader.GetString()!;
48-
if (propertyName == KeyName)
79+
if (FoundKeyProperty(propertyName, caseInsensitiveMatch))
4980
{
5081
reader.ReadWithVerify();
51-
k = JsonSerializer.Deserialize<TKey>(ref reader, options, ref state, KeyName);
82+
k = JsonSerializer.Deserialize<TKey>(ref reader, options, ref state, _keyName);
5283
keySet = true;
5384
}
54-
else if (propertyName == ValueName)
85+
else if (FoundValueProperty(propertyName, caseInsensitiveMatch))
5586
{
5687
reader.ReadWithVerify();
57-
v = JsonSerializer.Deserialize<TValue>(ref reader, options, ref state, ValueName);
88+
v = JsonSerializer.Deserialize<TValue>(ref reader, options, ref state, _valueName);
5889
valueSet = true;
5990
}
6091
else
@@ -70,28 +101,21 @@ internal override bool OnTryRead(
70101
}
71102

72103
propertyName = reader.GetString()!;
73-
if (propertyName == KeyName)
104+
if (!keySet && FoundKeyProperty(propertyName, caseInsensitiveMatch))
74105
{
75106
reader.ReadWithVerify();
76-
k = JsonSerializer.Deserialize<TKey>(ref reader, options, ref state, KeyName);
77-
keySet = true;
107+
k = JsonSerializer.Deserialize<TKey>(ref reader, options, ref state, _keyName);
78108
}
79-
else if (propertyName == ValueName)
109+
else if (!valueSet && FoundValueProperty(propertyName, caseInsensitiveMatch))
80110
{
81111
reader.ReadWithVerify();
82-
v = JsonSerializer.Deserialize<TValue>(ref reader, options, ref state, ValueName);
83-
valueSet = true;
112+
v = JsonSerializer.Deserialize<TValue>(ref reader, options, ref state, _valueName);
84113
}
85114
else
86115
{
87116
ThrowHelper.ThrowJsonException();
88117
}
89118

90-
if (!keySet || !valueSet)
91-
{
92-
ThrowHelper.ThrowJsonException();
93-
}
94-
95119
reader.ReadWithVerify();
96120

97121
if (reader.TokenType != JsonTokenType.EndObject)
@@ -107,14 +131,28 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, KeyValuePair<TKey, TVal
107131
{
108132
writer.WriteStartObject();
109133

110-
writer.WritePropertyName(_keyName);
111-
JsonSerializer.Serialize(writer, value.Key, options, ref state, KeyName);
134+
writer.WritePropertyName(_keyNameEncoded);
135+
JsonSerializer.Serialize(writer, value.Key, options, ref state, _keyName);
112136

113-
writer.WritePropertyName(_valueName);
114-
JsonSerializer.Serialize(writer, value.Value, options, ref state, ValueName);
137+
writer.WritePropertyName(_valueNameEncoded);
138+
JsonSerializer.Serialize(writer, value.Value, options, ref state, _valueName);
115139

116140
writer.WriteEndObject();
117141
return true;
118142
}
143+
144+
private bool FoundKeyProperty(string propertyName, bool caseInsensitiveMatch)
145+
{
146+
return propertyName == _keyName ||
147+
(caseInsensitiveMatch && string.Equals(propertyName, _keyName, StringComparison.OrdinalIgnoreCase)) ||
148+
propertyName == KeyNameCLR;
149+
}
150+
151+
private bool FoundValueProperty(string propertyName, bool caseInsensitiveMatch)
152+
{
153+
return propertyName == _valueName ||
154+
(caseInsensitiveMatch && string.Equals(propertyName, _valueName, StringComparison.OrdinalIgnoreCase)) ||
155+
propertyName == ValueNameCLR;
156+
}
119157
}
120158
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/KeyValuePairConverterFactory.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public override JsonConverter CreateConverter(Type type, JsonSerializerOptions o
3232
args: null,
3333
culture: null)!;
3434

35+
converter.Initialize(options);
36+
3537
return converter;
3638
}
3739
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,7 @@ internal bool ShouldFlush(Utf8JsonWriter writer, ref WriteStack state)
7373
internal virtual bool ConstructorIsParameterized => false;
7474

7575
internal ConstructorInfo? ConstructorInfo { get; set; }
76+
77+
internal virtual void Initialize(JsonSerializerOptions options) { }
7678
}
7779
}

src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,9 @@ public static void ThrowInvalidOperationException_SerializerPropertyNameNull(Typ
163163

164164
[DoesNotReturn]
165165
[MethodImpl(MethodImplOptions.NoInlining)]
166-
public static void ThrowInvalidOperationException_SerializerDictionaryKeyNull(Type policyType)
166+
public static void ThrowInvalidOperationException_NamingPolicyReturnNull(JsonNamingPolicy namingPolicy)
167167
{
168-
throw new InvalidOperationException(SR.Format(SR.SerializerDictionaryKeyNull, policyType));
168+
throw new InvalidOperationException(SR.Format(SR.NamingPolicyReturnNull, namingPolicy));
169169
}
170170

171171
[DoesNotReturn]

src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs

Lines changed: 0 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -962,167 +962,6 @@ public static void ReadSimpleSortedSetT()
962962
Assert.Equal(0, result.Count());
963963
}
964964

965-
[Fact]
966-
public static void ReadSimpleKeyValuePairFail()
967-
{
968-
// Invalid form: no Value
969-
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<KeyValuePair<string, int>>(@"{""Key"": 123}"));
970-
971-
// Invalid form: extra property
972-
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<KeyValuePair<string, int>>(@"{""Key"": ""Key"", ""Value"": 123, ""Value2"": 456}"));
973-
974-
// Invalid form: does not contain both Key and Value properties
975-
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<KeyValuePair<string, int>>(@"{""Key"": ""Key"", ""Val"": 123"));
976-
}
977-
978-
[Fact]
979-
public static void ReadListOfKeyValuePair()
980-
{
981-
List<KeyValuePair<string, int>> input = JsonSerializer.Deserialize<List<KeyValuePair<string, int>>>(@"[{""Key"": ""123"", ""Value"": 123},{""Key"": ""456"", ""Value"": 456}]");
982-
983-
Assert.Equal(2, input.Count);
984-
Assert.Equal("123", input[0].Key);
985-
Assert.Equal(123, input[0].Value);
986-
Assert.Equal("456", input[1].Key);
987-
Assert.Equal(456, input[1].Value);
988-
}
989-
990-
[Fact]
991-
public static void ReadKeyValuePairOfList()
992-
{
993-
KeyValuePair<string, List<int>> input = JsonSerializer.Deserialize<KeyValuePair<string, List<int>>>(@"{""Key"":""Key"", ""Value"":[1, 2, 3]}");
994-
995-
Assert.Equal("Key", input.Key);
996-
Assert.Equal(3, input.Value.Count);
997-
Assert.Equal(1, input.Value[0]);
998-
Assert.Equal(2, input.Value[1]);
999-
Assert.Equal(3, input.Value[2]);
1000-
}
1001-
1002-
[Theory]
1003-
[InlineData(@"{""Key"":""Key"", ""Value"":{""Key"":1, ""Value"":2}}")]
1004-
[InlineData(@"{""Key"":""Key"", ""Value"":{""Value"":2, ""Key"":1}}")]
1005-
[InlineData(@"{""Value"":{""Key"":1, ""Value"":2}, ""Key"":""Key""}")]
1006-
[InlineData(@"{""Value"":{""Value"":2, ""Key"":1}, ""Key"":""Key""}")]
1007-
public static void ReadKeyValuePairOfKeyValuePair(string json)
1008-
{
1009-
KeyValuePair<string, KeyValuePair<int, int>> input = JsonSerializer.Deserialize<KeyValuePair<string, KeyValuePair<int, int>>>(json);
1010-
1011-
Assert.Equal("Key", input.Key);
1012-
Assert.Equal(1, input.Value.Key);
1013-
Assert.Equal(2, input.Value.Value);
1014-
}
1015-
1016-
[Fact]
1017-
public static void ReadKeyValuePairWithNullValues()
1018-
{
1019-
{
1020-
KeyValuePair<string, string> kvp = JsonSerializer.Deserialize<KeyValuePair<string, string>>(@"{""Key"":""key"",""Value"":null}");
1021-
Assert.Equal("key", kvp.Key);
1022-
Assert.Null(kvp.Value);
1023-
}
1024-
1025-
{
1026-
KeyValuePair<string, object> kvp = JsonSerializer.Deserialize<KeyValuePair<string, object>>(@"{""Key"":""key"",""Value"":null}");
1027-
Assert.Equal("key", kvp.Key);
1028-
Assert.Null(kvp.Value);
1029-
}
1030-
1031-
{
1032-
KeyValuePair<string, SimpleClassWithKeyValuePairs> kvp = JsonSerializer.Deserialize<KeyValuePair<string, SimpleClassWithKeyValuePairs>>(@"{""Key"":""key"",""Value"":null}");
1033-
Assert.Equal("key", kvp.Key);
1034-
Assert.Null(kvp.Value);
1035-
}
1036-
1037-
{
1038-
KeyValuePair<string, KeyValuePair<string, string>> kvp = JsonSerializer.Deserialize<KeyValuePair<string, KeyValuePair<string, string>>>(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}");
1039-
Assert.Equal("key", kvp.Key);
1040-
Assert.Equal("key", kvp.Value.Key);
1041-
Assert.Null(kvp.Value.Value);
1042-
}
1043-
1044-
{
1045-
KeyValuePair<string, KeyValuePair<string, object>> kvp = JsonSerializer.Deserialize<KeyValuePair<string, KeyValuePair<string, object>>>(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}");
1046-
Assert.Equal("key", kvp.Key);
1047-
Assert.Equal("key", kvp.Value.Key);
1048-
Assert.Null(kvp.Value.Value);
1049-
}
1050-
1051-
{
1052-
KeyValuePair<string, KeyValuePair<string, SimpleClassWithKeyValuePairs>> kvp = JsonSerializer.Deserialize<KeyValuePair<string, KeyValuePair<string, SimpleClassWithKeyValuePairs>>>(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}");
1053-
Assert.Equal("key", kvp.Key);
1054-
Assert.Equal("key", kvp.Value.Key);
1055-
Assert.Null(kvp.Value.Value);
1056-
}
1057-
}
1058-
1059-
[Fact]
1060-
public static void ReadClassWithNullKeyValuePairValues()
1061-
{
1062-
string json =
1063-
@"{" +
1064-
@"""KvpWStrVal"":{" +
1065-
@"""Key"":""key""," +
1066-
@"""Value"":null" +
1067-
@"}," +
1068-
@"""KvpWObjVal"":{" +
1069-
@"""Key"":""key""," +
1070-
@"""Value"":null" +
1071-
@"}," +
1072-
@"""KvpWClassVal"":{" +
1073-
@"""Key"":""key""," +
1074-
@"""Value"":null" +
1075-
@"}," +
1076-
@"""KvpWStrKvpVal"":{" +
1077-
@"""Key"":""key""," +
1078-
@"""Value"":{" +
1079-
@"""Key"":""key""," +
1080-
@"""Value"":null" +
1081-
@"}" +
1082-
@"}," +
1083-
@"""KvpWObjKvpVal"":{" +
1084-
@"""Key"":""key""," +
1085-
@"""Value"":{" +
1086-
@"""Key"":""key""," +
1087-
@"""Value"":null" +
1088-
@"}" +
1089-
@"}," +
1090-
@"""KvpWClassKvpVal"":{" +
1091-
@"""Key"":""key""," +
1092-
@"""Value"":{" +
1093-
@"""Key"":""key""," +
1094-
@"""Value"":null" +
1095-
@"}" +
1096-
@"}" +
1097-
@"}";
1098-
SimpleClassWithKeyValuePairs obj = JsonSerializer.Deserialize<SimpleClassWithKeyValuePairs>(json);
1099-
1100-
Assert.Equal("key", obj.KvpWStrVal.Key);
1101-
Assert.Equal("key", obj.KvpWObjVal.Key);
1102-
Assert.Equal("key", obj.KvpWClassVal.Key);
1103-
Assert.Equal("key", obj.KvpWStrKvpVal.Key);
1104-
Assert.Equal("key", obj.KvpWObjKvpVal.Key);
1105-
Assert.Equal("key", obj.KvpWClassKvpVal.Key);
1106-
Assert.Equal("key", obj.KvpWStrKvpVal.Value.Key);
1107-
Assert.Equal("key", obj.KvpWObjKvpVal.Value.Key);
1108-
Assert.Equal("key", obj.KvpWClassKvpVal.Value.Key);
1109-
1110-
Assert.Null(obj.KvpWStrVal.Value);
1111-
Assert.Null(obj.KvpWObjVal.Value);
1112-
Assert.Null(obj.KvpWClassVal.Value);
1113-
Assert.Null(obj.KvpWStrKvpVal.Value.Value);
1114-
Assert.Null(obj.KvpWObjKvpVal.Value.Value);
1115-
Assert.Null(obj.KvpWClassKvpVal.Value.Value);
1116-
}
1117-
1118-
[Fact]
1119-
public static void Kvp_NullKeyIsFine()
1120-
{
1121-
KeyValuePair<string, string> kvp = JsonSerializer.Deserialize<KeyValuePair<string, string>>(@"{""Key"":null,""Value"":null}");
1122-
Assert.Null(kvp.Key);
1123-
Assert.Null(kvp.Value);
1124-
}
1125-
1126965
[Fact]
1127966
public static void ReadSimpleTestClass_GenericCollectionWrappers()
1128967
{

0 commit comments

Comments
 (0)