diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx
index 59b88fa60bf186..fc1519d0df9cd4 100644
--- a/src/libraries/System.Text.Json/src/Resources/Strings.resx
+++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx
@@ -345,6 +345,9 @@
The JSON property name for '{0}.{1}' cannot be null.
+
+ The property naming policy '{0}' cannot return a null property name.
+
An item with the same property name '{0}' has already been added.
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonKeyValuePairConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonKeyValuePairConverter.cs
index 8fa37cbfb586f3..e80cf26cc1ee86 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonKeyValuePairConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonKeyValuePairConverter.cs
@@ -10,6 +10,43 @@ namespace System.Text.Json.Serialization.Converters
{
internal sealed class JsonKeyValuePairConverter : JsonConverterFactory
{
+ private readonly byte[] _keyName;
+ private readonly byte[] _valueName;
+
+ private readonly JsonEncodedText _encodedKeyName;
+ private readonly JsonEncodedText _encodedValueName;
+
+ public JsonKeyValuePairConverter(JsonSerializerOptions options)
+ {
+ JsonNamingPolicy namingPolicy = options.PropertyNamingPolicy;
+ if (namingPolicy == null)
+ {
+ _keyName = new byte[] { (byte)'K', (byte)'e', (byte)'y' };
+ _valueName = new byte[] { (byte)'V', (byte)'a', (byte)'l', (byte)'u', (byte)'e' };
+ }
+ else
+ {
+ string propertyName = namingPolicy.ConvertName("Key");
+ if (propertyName == null)
+ {
+ ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNamingPolicyReturnNull(namingPolicy);
+ }
+ _keyName = JsonReaderHelper.s_utf8Encoding.GetBytes(propertyName);
+
+ propertyName = namingPolicy.ConvertName("Value");
+ if (propertyName == null)
+ {
+ ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNamingPolicyReturnNull(namingPolicy);
+ }
+ _valueName = JsonReaderHelper.s_utf8Encoding.GetBytes(propertyName);
+ }
+
+ // "encoder: null" is used since the literal values of "Key" and "Value" should not normally be escaped
+ // unless a custom encoder is used that escapes these ASCII characters (rare).
+ _encodedKeyName = JsonEncodedText.Encode(_keyName, encoder: null);
+ _encodedValueName = JsonEncodedText.Encode(_valueName, encoder: null);
+ }
+
public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.IsGenericType)
@@ -19,7 +56,9 @@ public override bool CanConvert(Type typeToConvert)
return (generic == typeof(KeyValuePair<,>));
}
- [PreserveDependency(".ctor()", "System.Text.Json.Serialization.Converters.JsonKeyValuePairConverter`2")]
+ [PreserveDependency(
+ ".ctor(Byte[], Byte[], System.Text.Json.JsonEncodedText, System.Text.Json.JsonEncodedText)",
+ "System.Text.Json.Serialization.Converters.JsonKeyValuePairConverter`2")]
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
{
Type keyType = type.GetGenericArguments()[0];
@@ -29,7 +68,7 @@ public override JsonConverter CreateConverter(Type type, JsonSerializerOptions o
typeof(JsonKeyValuePairConverter<,>).MakeGenericType(new Type[] { keyType, valueType }),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
- args: null,
+ args: new object[] { _keyName, _valueName, _encodedKeyName, _encodedValueName },
culture: null);
return converter;
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterKeyValuePair.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterKeyValuePair.cs
index ff9fe09bfa61b0..4958d410b2da2a 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterKeyValuePair.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterKeyValuePair.cs
@@ -8,14 +8,19 @@ namespace System.Text.Json.Serialization.Converters
{
internal sealed class JsonKeyValuePairConverter : JsonConverter>
{
- private const string KeyName = "Key";
- private const string ValueName = "Value";
+ private readonly byte[] _keyName;
+ private readonly byte[] _valueName;
- // "encoder: null" is used since the literal values of "Key" and "Value" should not normally be escaped
- // unless a custom encoder is used that escapes these ASCII characters (rare).
- // Also by not specifying an encoder allows the values to be cached statically here.
- private static readonly JsonEncodedText _keyName = JsonEncodedText.Encode(KeyName, encoder: null);
- private static readonly JsonEncodedText _valueName = JsonEncodedText.Encode(ValueName, encoder: null);
+ private readonly JsonEncodedText _encodedKeyName;
+ private readonly JsonEncodedText _encodedValueName;
+
+ public JsonKeyValuePairConverter(byte[] keyName, byte[] valueName, JsonEncodedText encodedKeyName, JsonEncodedText encodedValueName)
+ {
+ _keyName = keyName;
+ _valueName = valueName;
+ _encodedKeyName = encodedKeyName;
+ _encodedValueName = encodedValueName;
+ }
public override KeyValuePair Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
@@ -37,13 +42,12 @@ public override KeyValuePair Read(ref Utf8JsonReader reader, Type
ThrowHelper.ThrowJsonException();
}
- string propertyName = reader.GetString();
- if (propertyName == KeyName)
+ if (reader.ValueTextEquals(_keyName))
{
k = ReadProperty(ref reader, typeToConvert, options);
keySet = true;
}
- else if (propertyName == ValueName)
+ else if (reader.ValueTextEquals(_valueName))
{
v = ReadProperty(ref reader, typeToConvert, options);
valueSet = true;
@@ -60,13 +64,12 @@ public override KeyValuePair Read(ref Utf8JsonReader reader, Type
ThrowHelper.ThrowJsonException();
}
- propertyName = reader.GetString();
- if (propertyName == ValueName)
+ if (reader.ValueTextEquals(_valueName))
{
v = ReadProperty(ref reader, typeToConvert, options);
valueSet = true;
}
- else if (propertyName == KeyName)
+ else if (reader.ValueTextEquals(_keyName))
{
k = ReadProperty(ref reader, typeToConvert, options);
keySet = true;
@@ -131,8 +134,8 @@ private void WriteProperty(Utf8JsonWriter writer, T value, JsonEncodedText na
public override void Write(Utf8JsonWriter writer, KeyValuePair value, JsonSerializerOptions options)
{
writer.WriteStartObject();
- WriteProperty(writer, value.Key, _keyName, options);
- WriteProperty(writer, value.Value, _valueName, options);
+ WriteProperty(writer, value.Key, _encodedKeyName, options);
+ WriteProperty(writer, value.Value, _encodedValueName, options);
writer.WriteEndObject();
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs
index 3605750fe5d38f..ff0e15cc16012e 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs
@@ -120,7 +120,7 @@ protected override void OnWriteDictionary(ref WriteStackFrame current, Utf8JsonW
if (key == null)
{
- ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(Options.DictionaryKeyPolicy.GetType());
+ ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(Options.DictionaryKeyPolicy);
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs
index 22c1f2324ceea7..f8ee206408686a 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs
@@ -161,7 +161,7 @@ internal static void WriteDictionary(
if (key == null)
{
- ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(options.DictionaryKeyPolicy.GetType());
+ ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(options.DictionaryKeyPolicy);
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
index 9be3015f510dc6..d0ae3fccda1489 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
@@ -16,15 +16,28 @@ namespace System.Text.Json
///
public sealed partial class JsonSerializerOptions
{
+ private List _defaultFactoryConverters;
+
// The global list of built-in simple converters.
private static readonly Dictionary s_defaultSimpleConverters = GetDefaultSimpleConverters();
- // The global list of built-in converters that override CanConvert().
- private static readonly List s_defaultFactoryConverters = GetDefaultConverters();
-
// The cached converters (custom or built-in).
private readonly ConcurrentDictionary _converters = new ConcurrentDictionary();
+ // The global list of built-in converters that override CanConvert().
+ private List DefaultFactoryConverters
+ {
+ get
+ {
+ if (_defaultFactoryConverters == null)
+ {
+ _defaultFactoryConverters = GetDefaultConverters();
+ }
+
+ return _defaultFactoryConverters;
+ }
+ }
+
private static Dictionary GetDefaultSimpleConverters()
{
var converters = new Dictionary(NumberOfSimpleConverters);
@@ -40,7 +53,7 @@ private static Dictionary GetDefaultSimpleConverters()
return converters;
}
- private static List GetDefaultConverters()
+ private List GetDefaultConverters()
{
const int NumberOfConverters = 2;
@@ -48,7 +61,7 @@ private static List GetDefaultConverters()
// Use a list for converters that implement CanConvert().
converters.Add(new JsonConverterEnum());
- converters.Add(new JsonKeyValuePairConverter());
+ converters.Add(new JsonKeyValuePairConverter(this));
// We will likely add collection converters here in the future.
@@ -140,7 +153,7 @@ public JsonConverter GetConverter(Type typeToConvert)
}
else
{
- foreach (JsonConverter item in s_defaultFactoryConverters)
+ foreach (JsonConverter item in DefaultFactoryConverters)
{
if (item.CanConvert(typeToConvert))
{
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
index 9cd7589737a4c0..fe4e36a19ccf98 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
@@ -125,10 +125,15 @@ public static void ThrowInvalidOperationException_SerializerPropertyNameNull(Typ
throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNameNull, parentType, jsonPropertyInfo.PropertyInfo.Name));
}
+ public static void ThrowInvalidOperationException_SerializerPropertyNamingPolicyReturnNull(JsonNamingPolicy namingPolicy)
+ {
+ throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNamingPolicyReturnNull, namingPolicy.GetType()));
+ }
+
[MethodImpl(MethodImplOptions.NoInlining)]
- public static void ThrowInvalidOperationException_SerializerDictionaryKeyNull(Type policyType)
+ public static void ThrowInvalidOperationException_SerializerDictionaryKeyNull(JsonNamingPolicy namingPolicy)
{
- throw new InvalidOperationException(SR.Format(SR.SerializerDictionaryKeyNull, policyType));
+ throw new InvalidOperationException(SR.Format(SR.SerializerDictionaryKeyNull, namingPolicy.GetType()));
}
[MethodImpl(MethodImplOptions.NoInlining)]
diff --git a/src/libraries/System.Text.Json/tests/Serialization/PropertyNameTests.cs b/src/libraries/System.Text.Json/tests/Serialization/PropertyNameTests.cs
index c5feea340c20c6..a6b797490510b8 100644
--- a/src/libraries/System.Text.Json/tests/Serialization/PropertyNameTests.cs
+++ b/src/libraries/System.Text.Json/tests/Serialization/PropertyNameTests.cs
@@ -430,6 +430,112 @@ public static void LongPropertyNames(int propertyLength, char ch)
string jsonRoundTripped = JsonSerializer.Serialize(obj, options);
Assert.Equal(json, jsonRoundTripped);
}
+
+ [Fact]
+ public static void RootKeyValuePairSerialize()
+ {
+ JsonSerializerOptions options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+
+ const string jsonWithoutPolicy = @"{""Key"":""MyKey"",""Value"":""MyValue""}";
+ const string jsonWithPolicy = @"{""key"":""MyKey"",""value"":""MyValue""}";
+
+ // Baseline: Without policy.
+ string serialized = JsonSerializer.Serialize(new KeyValuePair("MyKey", "MyValue"));
+ Assert.Equal(jsonWithoutPolicy, serialized);
+
+ KeyValuePair kvp = JsonSerializer.Deserialize>(serialized);
+ Assert.Equal("MyKey", kvp.Key);
+ Assert.Equal("MyValue", kvp.Value);
+
+ // No property name match.
+ Assert.Throws(() => JsonSerializer.Deserialize>(jsonWithPolicy));
+
+ // With policy.
+ serialized = JsonSerializer.Serialize(new KeyValuePair("MyKey", "MyValue"), options);
+ Assert.Equal(jsonWithPolicy, serialized);
+
+ kvp = JsonSerializer.Deserialize>(serialized, options);
+ Assert.Equal("MyKey", kvp.Key);
+ Assert.Equal("MyValue", kvp.Value);
+
+ // No property name match.
+ Assert.Throws(() => JsonSerializer.Deserialize>(jsonWithoutPolicy, options));
+ }
+
+ [Fact]
+ public static void KeyValuePairAsCollectionElementSerialize()
+ {
+ JsonSerializerOptions options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+
+ const string jsonWithoutPolicy = @"[{""Key"":""MyKey"",""Value"":""MyValue""}]";
+ const string jsonWithPolicy = @"[{""key"":""MyKey"",""value"":""MyValue""}]";
+
+ // Baseline: Without policy.
+ string serialized = JsonSerializer.Serialize(new KeyValuePair[] { new KeyValuePair("MyKey", "MyValue") });
+ Assert.Equal(jsonWithoutPolicy, serialized);
+
+ KeyValuePair[] arr = JsonSerializer.Deserialize[]>(serialized);
+ Assert.Equal("MyKey", arr[0].Key);
+ Assert.Equal("MyValue", arr[0].Value);
+
+ // No property name match.
+ Assert.Throws(() => JsonSerializer.Deserialize[]>(jsonWithPolicy));
+
+ // With policy.
+ serialized = JsonSerializer.Serialize(new KeyValuePair[] { new KeyValuePair("MyKey", "MyValue") }, options);
+ Assert.Equal(jsonWithPolicy, serialized);
+
+ arr = JsonSerializer.Deserialize[]>(serialized, options);
+ Assert.Equal("MyKey", arr[0].Key);
+ Assert.Equal("MyValue", arr[0].Value);
+
+ // No property name match.
+ Assert.Throws(() => JsonSerializer.Deserialize[]>(jsonWithoutPolicy, options));
+ }
+
+ [Fact]
+ public static void KeyValuePairAsClassPropertyElementSerialize()
+ {
+ JsonSerializerOptions options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+
+ const string jsonWithoutPolicy = @"{""Kvp"":{""Key"":""MyKey"",""Value"":""MyValue""}}";
+ const string jsonWithPolicy = @"{""kvp"":{""key"":""MyKey"",""value"":""MyValue""}}";
+
+ // Baseline: Without policy.
+ string serialized = JsonSerializer.Serialize(new ClassWithKVP { Kvp = new KeyValuePair("MyKey", "MyValue") });
+ Assert.Equal(jsonWithoutPolicy, serialized);
+
+ ClassWithKVP obj = JsonSerializer.Deserialize(serialized);
+ Assert.Equal("MyKey", obj.Kvp.Key);
+ Assert.Equal("MyValue", obj.Kvp.Value);
+
+ // No property name match.
+ obj = JsonSerializer.Deserialize(jsonWithPolicy);
+ Assert.Null(obj.Kvp.Key);
+ Assert.Null(obj.Kvp.Value);
+
+ // With policy.
+ serialized = JsonSerializer.Serialize(new ClassWithKVP { Kvp = new KeyValuePair("MyKey", "MyValue") }, options);
+ Assert.Equal(jsonWithPolicy, serialized);
+
+ obj = JsonSerializer.Deserialize(serialized, options);
+ Assert.Equal("MyKey", obj.Kvp.Key);
+ Assert.Equal("MyValue", obj.Kvp.Value);
+
+ // No property name match.
+ obj = JsonSerializer.Deserialize(jsonWithoutPolicy, options);
+ Assert.Null(obj.Kvp.Key);
+ Assert.Null(obj.Kvp.Value);
+ }
}
public class OverridePropertyNameDesignTime_TestClass
@@ -495,4 +601,9 @@ public class EmptyClassWithExtensionProperty
[JsonExtensionData]
public IDictionary MyOverflow { get; set; }
}
+
+ public class ClassWithKVP
+ {
+ public KeyValuePair Kvp { get; set; }
+ }
}