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