diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index b34b706d927507..44de8a1f85fb79 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -396,8 +396,8 @@ The converter '{0}' wrote too much or not enough. - - The dictionary key policy '{0}' cannot return a null key. + + The naming policy '{0}' cannot return null. The attribute '{0}' cannot exist more than once on '{1}'. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs index a657398ca2d3f8..4603b59c82f960 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs @@ -47,7 +47,7 @@ protected string GetKeyName(string key, ref WriteStack state, JsonSerializerOpti if (key == null) { - ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(options.DictionaryKeyPolicy.GetType()); + ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(options.DictionaryKeyPolicy); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/KeyValuePairConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/KeyValuePairConverter.cs index f74a7de0cdbe1e..08e790738f62fa 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/KeyValuePairConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/KeyValuePairConverter.cs @@ -3,23 +3,52 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Text.Encodings.Web; namespace System.Text.Json.Serialization.Converters { internal sealed class KeyValuePairConverter : JsonValueConverter> { - private const string KeyName = "Key"; - private const string ValueName = "Value"; + private const string KeyNameCLR = "Key"; + private const string ValueNameCLR = "Value"; - // todo: https://github.com/dotnet/runtime/issues/1197 - // move these to JsonSerializerOptions and use the proper encoding. - private static readonly JsonEncodedText _keyName = JsonEncodedText.Encode(KeyName, encoder: null); - private static readonly JsonEncodedText _valueName = JsonEncodedText.Encode(ValueName, encoder: null); + // Property name for "Key" and "Value" with Options.PropertyNamingPolicy applied. + private string _keyName = null!; + private string _valueName = null!; + + // _keyName and _valueName as JsonEncodedText. + private JsonEncodedText _keyNameEncoded; + private JsonEncodedText _valueNameEncoded; // todo: https://github.com/dotnet/runtime/issues/32352 // it is possible to cache the underlying converters since this is an internal converter and // an instance is created only once for each JsonSerializerOptions instance. + internal override void Initialize(JsonSerializerOptions options) + { + JsonNamingPolicy? namingPolicy = options.PropertyNamingPolicy; + + if (namingPolicy == null) + { + _keyName = KeyNameCLR; + _valueName = ValueNameCLR; + } + else + { + _keyName = namingPolicy.ConvertName(KeyNameCLR); + _valueName = namingPolicy.ConvertName(ValueNameCLR); + + if (_keyName == null || _valueName == null) + { + ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(namingPolicy); + } + } + + JavaScriptEncoder? encoder = options.Encoder; + _keyNameEncoded = JsonEncodedText.Encode(_keyName, encoder); + _valueNameEncoded = JsonEncodedText.Encode(_valueName, encoder); + } + internal override bool OnTryRead( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, @@ -44,17 +73,19 @@ internal override bool OnTryRead( ThrowHelper.ThrowJsonException(); } + bool caseInsensitiveMatch = options.PropertyNameCaseInsensitive; + string propertyName = reader.GetString()!; - if (propertyName == KeyName) + if (FoundKeyProperty(propertyName, caseInsensitiveMatch)) { reader.ReadWithVerify(); - k = JsonSerializer.Deserialize(ref reader, options, ref state, KeyName); + k = JsonSerializer.Deserialize(ref reader, options, ref state, _keyName); keySet = true; } - else if (propertyName == ValueName) + else if (FoundValueProperty(propertyName, caseInsensitiveMatch)) { reader.ReadWithVerify(); - v = JsonSerializer.Deserialize(ref reader, options, ref state, ValueName); + v = JsonSerializer.Deserialize(ref reader, options, ref state, _valueName); valueSet = true; } else @@ -70,28 +101,21 @@ internal override bool OnTryRead( } propertyName = reader.GetString()!; - if (propertyName == KeyName) + if (!keySet && FoundKeyProperty(propertyName, caseInsensitiveMatch)) { reader.ReadWithVerify(); - k = JsonSerializer.Deserialize(ref reader, options, ref state, KeyName); - keySet = true; + k = JsonSerializer.Deserialize(ref reader, options, ref state, _keyName); } - else if (propertyName == ValueName) + else if (!valueSet && FoundValueProperty(propertyName, caseInsensitiveMatch)) { reader.ReadWithVerify(); - v = JsonSerializer.Deserialize(ref reader, options, ref state, ValueName); - valueSet = true; + v = JsonSerializer.Deserialize(ref reader, options, ref state, _valueName); } else { ThrowHelper.ThrowJsonException(); } - if (!keySet || !valueSet) - { - ThrowHelper.ThrowJsonException(); - } - reader.ReadWithVerify(); if (reader.TokenType != JsonTokenType.EndObject) @@ -107,14 +131,28 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, KeyValuePair false; internal ConstructorInfo? ConstructorInfo { get; set; } + + internal virtual void Initialize(JsonSerializerOptions options) { } } } 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 8d0bb4d01b4033..0d7bd68400ee44 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 @@ -163,9 +163,9 @@ public static void ThrowInvalidOperationException_SerializerPropertyNameNull(Typ [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowInvalidOperationException_SerializerDictionaryKeyNull(Type policyType) + public static void ThrowInvalidOperationException_NamingPolicyReturnNull(JsonNamingPolicy namingPolicy) { - throw new InvalidOperationException(SR.Format(SR.SerializerDictionaryKeyNull, policyType)); + throw new InvalidOperationException(SR.Format(SR.NamingPolicyReturnNull, namingPolicy)); } [DoesNotReturn] diff --git a/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs index d1cb10248613af..3e9eae828e837e 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs @@ -962,167 +962,6 @@ public static void ReadSimpleSortedSetT() Assert.Equal(0, result.Count()); } - [Fact] - public static void ReadSimpleKeyValuePairFail() - { - // Invalid form: no Value - Assert.Throws(() => JsonSerializer.Deserialize>(@"{""Key"": 123}")); - - // Invalid form: extra property - Assert.Throws(() => JsonSerializer.Deserialize>(@"{""Key"": ""Key"", ""Value"": 123, ""Value2"": 456}")); - - // Invalid form: does not contain both Key and Value properties - Assert.Throws(() => JsonSerializer.Deserialize>(@"{""Key"": ""Key"", ""Val"": 123")); - } - - [Fact] - public static void ReadListOfKeyValuePair() - { - List> input = JsonSerializer.Deserialize>>(@"[{""Key"": ""123"", ""Value"": 123},{""Key"": ""456"", ""Value"": 456}]"); - - Assert.Equal(2, input.Count); - Assert.Equal("123", input[0].Key); - Assert.Equal(123, input[0].Value); - Assert.Equal("456", input[1].Key); - Assert.Equal(456, input[1].Value); - } - - [Fact] - public static void ReadKeyValuePairOfList() - { - KeyValuePair> input = JsonSerializer.Deserialize>>(@"{""Key"":""Key"", ""Value"":[1, 2, 3]}"); - - Assert.Equal("Key", input.Key); - Assert.Equal(3, input.Value.Count); - Assert.Equal(1, input.Value[0]); - Assert.Equal(2, input.Value[1]); - Assert.Equal(3, input.Value[2]); - } - - [Theory] - [InlineData(@"{""Key"":""Key"", ""Value"":{""Key"":1, ""Value"":2}}")] - [InlineData(@"{""Key"":""Key"", ""Value"":{""Value"":2, ""Key"":1}}")] - [InlineData(@"{""Value"":{""Key"":1, ""Value"":2}, ""Key"":""Key""}")] - [InlineData(@"{""Value"":{""Value"":2, ""Key"":1}, ""Key"":""Key""}")] - public static void ReadKeyValuePairOfKeyValuePair(string json) - { - KeyValuePair> input = JsonSerializer.Deserialize>>(json); - - Assert.Equal("Key", input.Key); - Assert.Equal(1, input.Value.Key); - Assert.Equal(2, input.Value.Value); - } - - [Fact] - public static void ReadKeyValuePairWithNullValues() - { - { - KeyValuePair kvp = JsonSerializer.Deserialize>(@"{""Key"":""key"",""Value"":null}"); - Assert.Equal("key", kvp.Key); - Assert.Null(kvp.Value); - } - - { - KeyValuePair kvp = JsonSerializer.Deserialize>(@"{""Key"":""key"",""Value"":null}"); - Assert.Equal("key", kvp.Key); - Assert.Null(kvp.Value); - } - - { - KeyValuePair kvp = JsonSerializer.Deserialize>(@"{""Key"":""key"",""Value"":null}"); - Assert.Equal("key", kvp.Key); - Assert.Null(kvp.Value); - } - - { - KeyValuePair> kvp = JsonSerializer.Deserialize>>(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}"); - Assert.Equal("key", kvp.Key); - Assert.Equal("key", kvp.Value.Key); - Assert.Null(kvp.Value.Value); - } - - { - KeyValuePair> kvp = JsonSerializer.Deserialize>>(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}"); - Assert.Equal("key", kvp.Key); - Assert.Equal("key", kvp.Value.Key); - Assert.Null(kvp.Value.Value); - } - - { - KeyValuePair> kvp = JsonSerializer.Deserialize>>(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}"); - Assert.Equal("key", kvp.Key); - Assert.Equal("key", kvp.Value.Key); - Assert.Null(kvp.Value.Value); - } - } - - [Fact] - public static void ReadClassWithNullKeyValuePairValues() - { - string json = - @"{" + - @"""KvpWStrVal"":{" + - @"""Key"":""key""," + - @"""Value"":null" + - @"}," + - @"""KvpWObjVal"":{" + - @"""Key"":""key""," + - @"""Value"":null" + - @"}," + - @"""KvpWClassVal"":{" + - @"""Key"":""key""," + - @"""Value"":null" + - @"}," + - @"""KvpWStrKvpVal"":{" + - @"""Key"":""key""," + - @"""Value"":{" + - @"""Key"":""key""," + - @"""Value"":null" + - @"}" + - @"}," + - @"""KvpWObjKvpVal"":{" + - @"""Key"":""key""," + - @"""Value"":{" + - @"""Key"":""key""," + - @"""Value"":null" + - @"}" + - @"}," + - @"""KvpWClassKvpVal"":{" + - @"""Key"":""key""," + - @"""Value"":{" + - @"""Key"":""key""," + - @"""Value"":null" + - @"}" + - @"}" + - @"}"; - SimpleClassWithKeyValuePairs obj = JsonSerializer.Deserialize(json); - - Assert.Equal("key", obj.KvpWStrVal.Key); - Assert.Equal("key", obj.KvpWObjVal.Key); - Assert.Equal("key", obj.KvpWClassVal.Key); - Assert.Equal("key", obj.KvpWStrKvpVal.Key); - Assert.Equal("key", obj.KvpWObjKvpVal.Key); - Assert.Equal("key", obj.KvpWClassKvpVal.Key); - Assert.Equal("key", obj.KvpWStrKvpVal.Value.Key); - Assert.Equal("key", obj.KvpWObjKvpVal.Value.Key); - Assert.Equal("key", obj.KvpWClassKvpVal.Value.Key); - - Assert.Null(obj.KvpWStrVal.Value); - Assert.Null(obj.KvpWObjVal.Value); - Assert.Null(obj.KvpWClassVal.Value); - Assert.Null(obj.KvpWStrKvpVal.Value.Value); - Assert.Null(obj.KvpWObjKvpVal.Value.Value); - Assert.Null(obj.KvpWClassKvpVal.Value.Value); - } - - [Fact] - public static void Kvp_NullKeyIsFine() - { - KeyValuePair kvp = JsonSerializer.Deserialize>(@"{""Key"":null,""Value"":null}"); - Assert.Null(kvp.Key); - Assert.Null(kvp.Value); - } - [Fact] public static void ReadSimpleTestClass_GenericCollectionWrappers() { diff --git a/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Write.cs b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Write.cs index e408f09b0e7fd0..2c2c61819d1ac6 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Write.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Write.cs @@ -750,117 +750,6 @@ public static void WritePrimitiveSortedSetT() Assert.Equal("[1,2]", json); } - [Fact] - public static void WritePrimitiveKeyValuePair() - { - KeyValuePair input = new KeyValuePair("Key", 123) ; - - string json = JsonSerializer.Serialize(input); - Assert.Equal(@"{""Key"":""Key"",""Value"":123}", json); - } - - [Fact] - public static void WriteListOfKeyValuePair() - { - List> input = new List> - { - new KeyValuePair("123", 123), - new KeyValuePair("456", 456) - }; - - string json = JsonSerializer.Serialize(input); - Assert.Equal(@"[{""Key"":""123"",""Value"":123},{""Key"":""456"",""Value"":456}]", json); - } - - [Fact] - public static void WriteKeyValuePairOfList() - { - KeyValuePair> input = new KeyValuePair>("Key", new List { 1, 2, 3 }); - - string json = JsonSerializer.Serialize(input); - Assert.Equal(@"{""Key"":""Key"",""Value"":[1,2,3]}", json); - } - - [Fact] - public static void WriteKeyValuePairOfKeyValuePair() - { - KeyValuePair> input = new KeyValuePair>( - "Key", new KeyValuePair("Key", 1)); - - string json = JsonSerializer.Serialize(input); - Assert.Equal(@"{""Key"":""Key"",""Value"":{""Key"":""Key"",""Value"":1}}", json); - } - - [Fact] - public static void WriteKeyValuePairWithNullValues() - { - { - KeyValuePair kvp = new KeyValuePair("key", null); - Assert.Equal(@"{""Key"":""key"",""Value"":null}", JsonSerializer.Serialize(kvp)); - } - - { - KeyValuePair kvp = new KeyValuePair("key", null); - Assert.Equal(@"{""Key"":""key"",""Value"":null}", JsonSerializer.Serialize(kvp)); - } - - { - KeyValuePair kvp = new KeyValuePair("key", null); - Assert.Equal(@"{""Key"":""key"",""Value"":null}", JsonSerializer.Serialize(kvp)); - } - - { - KeyValuePair> kvp = new KeyValuePair>("key", new KeyValuePair("key", null)); - Assert.Equal(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}", JsonSerializer.Serialize(kvp)); - } - - { - KeyValuePair> kvp = new KeyValuePair>("key", new KeyValuePair("key", null)); - Assert.Equal(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}", JsonSerializer.Serialize(kvp)); - } - - { - KeyValuePair> kvp = new KeyValuePair>("key", new KeyValuePair("key", null)); - Assert.Equal(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}", JsonSerializer.Serialize(kvp)); - } - } - - // https://github.com/dotnet/runtime/issues/30388 - [Fact] - public static void WriteClassWithNullKeyValuePairValues_NullWrittenAsEmptyObject() - { - var value = new SimpleClassWithKeyValuePairs() - { - KvpWStrVal = new KeyValuePair("key", null), - KvpWObjVal = new KeyValuePair("key", null), - KvpWClassVal = new KeyValuePair("key", null), - KvpWStrKvpVal = new KeyValuePair>("key", new KeyValuePair("key", null)), - KvpWObjKvpVal = new KeyValuePair>("key", new KeyValuePair("key", null)), - KvpWClassKvpVal = new KeyValuePair>("key", new KeyValuePair("key", null)), - }; - - string result = JsonSerializer.Serialize(value); - - // Roundtrip to ensure serialize was correct. - value = JsonSerializer.Deserialize(result); - Assert.Equal("key", value.KvpWStrVal.Key); - Assert.Equal("key", value.KvpWObjVal.Key); - Assert.Equal("key", value.KvpWClassVal.Key); - Assert.Equal("key", value.KvpWStrKvpVal.Key); - Assert.Equal("key", value.KvpWObjKvpVal.Key); - Assert.Equal("key", value.KvpWClassKvpVal.Key); - Assert.Equal("key", value.KvpWStrKvpVal.Value.Key); - Assert.Equal("key", value.KvpWObjKvpVal.Value.Key); - Assert.Equal("key", value.KvpWClassKvpVal.Value.Key); - - Assert.Null(value.KvpWStrVal.Value); - Assert.Null(value.KvpWObjVal.Value); - Assert.Null(value.KvpWClassVal.Value); - Assert.Null(value.KvpWStrKvpVal.Value.Value); - Assert.Null(value.KvpWObjKvpVal.Value.Value); - Assert.Null(value.KvpWClassKvpVal.Value.Value); - } - [Fact] public static void WriteGenericCollectionWrappers() { diff --git a/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.KeyValuePair.cs b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.KeyValuePair.cs new file mode 100644 index 00000000000000..868167086cfa9c --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.KeyValuePair.cs @@ -0,0 +1,438 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Text.Encodings.Web; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class CollectionTests + { + [Fact] + public static void ReadSimpleKeyValuePairFail() + { + // Invalid form: no Value + Assert.Throws(() => JsonSerializer.Deserialize>(@"{""Key"": 123}")); + + // Invalid form: extra property + Assert.Throws(() => JsonSerializer.Deserialize>(@"{""Key"": ""Key"", ""Value"": 123, ""Value2"": 456}")); + + // Invalid form: does not contain both Key and Value properties + Assert.Throws(() => JsonSerializer.Deserialize>(@"{""Key"": ""Key"", ""Val"": 123")); + } + + [Fact] + public static void ReadListOfKeyValuePair() + { + List> input = JsonSerializer.Deserialize>>(@"[{""Key"": ""123"", ""Value"": 123},{""Key"": ""456"", ""Value"": 456}]"); + + Assert.Equal(2, input.Count); + Assert.Equal("123", input[0].Key); + Assert.Equal(123, input[0].Value); + Assert.Equal("456", input[1].Key); + Assert.Equal(456, input[1].Value); + } + + [Fact] + public static void ReadKeyValuePairOfList() + { + KeyValuePair> input = JsonSerializer.Deserialize>>(@"{""Key"":""Key"", ""Value"":[1, 2, 3]}"); + + Assert.Equal("Key", input.Key); + Assert.Equal(3, input.Value.Count); + Assert.Equal(1, input.Value[0]); + Assert.Equal(2, input.Value[1]); + Assert.Equal(3, input.Value[2]); + } + + [Theory] + [InlineData(@"{""Key"":""Key"", ""Value"":{""Key"":1, ""Value"":2}}")] + [InlineData(@"{""Key"":""Key"", ""Value"":{""Value"":2, ""Key"":1}}")] + [InlineData(@"{""Value"":{""Key"":1, ""Value"":2}, ""Key"":""Key""}")] + [InlineData(@"{""Value"":{""Value"":2, ""Key"":1}, ""Key"":""Key""}")] + public static void ReadKeyValuePairOfKeyValuePair(string json) + { + KeyValuePair> input = JsonSerializer.Deserialize>>(json); + + Assert.Equal("Key", input.Key); + Assert.Equal(1, input.Value.Key); + Assert.Equal(2, input.Value.Value); + } + + [Fact] + public static void ReadKeyValuePairWithNullValues() + { + { + KeyValuePair kvp = JsonSerializer.Deserialize>(@"{""Key"":""key"",""Value"":null}"); + Assert.Equal("key", kvp.Key); + Assert.Null(kvp.Value); + } + + { + KeyValuePair kvp = JsonSerializer.Deserialize>(@"{""Key"":""key"",""Value"":null}"); + Assert.Equal("key", kvp.Key); + Assert.Null(kvp.Value); + } + + { + KeyValuePair kvp = JsonSerializer.Deserialize>(@"{""Key"":""key"",""Value"":null}"); + Assert.Equal("key", kvp.Key); + Assert.Null(kvp.Value); + } + + { + KeyValuePair> kvp = JsonSerializer.Deserialize>>(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}"); + Assert.Equal("key", kvp.Key); + Assert.Equal("key", kvp.Value.Key); + Assert.Null(kvp.Value.Value); + } + + { + KeyValuePair> kvp = JsonSerializer.Deserialize>>(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}"); + Assert.Equal("key", kvp.Key); + Assert.Equal("key", kvp.Value.Key); + Assert.Null(kvp.Value.Value); + } + + { + KeyValuePair> kvp = JsonSerializer.Deserialize>>(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}"); + Assert.Equal("key", kvp.Key); + Assert.Equal("key", kvp.Value.Key); + Assert.Null(kvp.Value.Value); + } + } + + [Fact] + public static void ReadClassWithNullKeyValuePairValues() + { + string json = + @"{" + + @"""KvpWStrVal"":{" + + @"""Key"":""key""," + + @"""Value"":null" + + @"}," + + @"""KvpWObjVal"":{" + + @"""Key"":""key""," + + @"""Value"":null" + + @"}," + + @"""KvpWClassVal"":{" + + @"""Key"":""key""," + + @"""Value"":null" + + @"}," + + @"""KvpWStrKvpVal"":{" + + @"""Key"":""key""," + + @"""Value"":{" + + @"""Key"":""key""," + + @"""Value"":null" + + @"}" + + @"}," + + @"""KvpWObjKvpVal"":{" + + @"""Key"":""key""," + + @"""Value"":{" + + @"""Key"":""key""," + + @"""Value"":null" + + @"}" + + @"}," + + @"""KvpWClassKvpVal"":{" + + @"""Key"":""key""," + + @"""Value"":{" + + @"""Key"":""key""," + + @"""Value"":null" + + @"}" + + @"}" + + @"}"; + SimpleClassWithKeyValuePairs obj = JsonSerializer.Deserialize(json); + + Assert.Equal("key", obj.KvpWStrVal.Key); + Assert.Equal("key", obj.KvpWObjVal.Key); + Assert.Equal("key", obj.KvpWClassVal.Key); + Assert.Equal("key", obj.KvpWStrKvpVal.Key); + Assert.Equal("key", obj.KvpWObjKvpVal.Key); + Assert.Equal("key", obj.KvpWClassKvpVal.Key); + Assert.Equal("key", obj.KvpWStrKvpVal.Value.Key); + Assert.Equal("key", obj.KvpWObjKvpVal.Value.Key); + Assert.Equal("key", obj.KvpWClassKvpVal.Value.Key); + + Assert.Null(obj.KvpWStrVal.Value); + Assert.Null(obj.KvpWObjVal.Value); + Assert.Null(obj.KvpWClassVal.Value); + Assert.Null(obj.KvpWStrKvpVal.Value.Value); + Assert.Null(obj.KvpWObjKvpVal.Value.Value); + Assert.Null(obj.KvpWClassKvpVal.Value.Value); + } + + [Fact] + public static void Kvp_NullKeyIsFine() + { + KeyValuePair kvp = JsonSerializer.Deserialize>(@"{""Key"":null,""Value"":null}"); + Assert.Null(kvp.Key); + Assert.Null(kvp.Value); + } + + [Fact] + public static void WritePrimitiveKeyValuePair() + { + KeyValuePair input = new KeyValuePair("Key", 123); + + string json = JsonSerializer.Serialize(input); + Assert.Equal(@"{""Key"":""Key"",""Value"":123}", json); + } + + [Fact] + public static void WriteListOfKeyValuePair() + { + List> input = new List> + { + new KeyValuePair("123", 123), + new KeyValuePair("456", 456) + }; + + string json = JsonSerializer.Serialize(input); + Assert.Equal(@"[{""Key"":""123"",""Value"":123},{""Key"":""456"",""Value"":456}]", json); + } + + [Fact] + public static void WriteKeyValuePairOfList() + { + KeyValuePair> input = new KeyValuePair>("Key", new List { 1, 2, 3 }); + + string json = JsonSerializer.Serialize(input); + Assert.Equal(@"{""Key"":""Key"",""Value"":[1,2,3]}", json); + } + + [Fact] + public static void WriteKeyValuePairOfKeyValuePair() + { + KeyValuePair> input = new KeyValuePair>( + "Key", new KeyValuePair("Key", 1)); + + string json = JsonSerializer.Serialize(input); + Assert.Equal(@"{""Key"":""Key"",""Value"":{""Key"":""Key"",""Value"":1}}", json); + } + + [Fact] + public static void WriteKeyValuePairWithNullValues() + { + { + KeyValuePair kvp = new KeyValuePair("key", null); + Assert.Equal(@"{""Key"":""key"",""Value"":null}", JsonSerializer.Serialize(kvp)); + } + + { + KeyValuePair kvp = new KeyValuePair("key", null); + Assert.Equal(@"{""Key"":""key"",""Value"":null}", JsonSerializer.Serialize(kvp)); + } + + { + KeyValuePair kvp = new KeyValuePair("key", null); + Assert.Equal(@"{""Key"":""key"",""Value"":null}", JsonSerializer.Serialize(kvp)); + } + + { + KeyValuePair> kvp = new KeyValuePair>("key", new KeyValuePair("key", null)); + Assert.Equal(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}", JsonSerializer.Serialize(kvp)); + } + + { + KeyValuePair> kvp = new KeyValuePair>("key", new KeyValuePair("key", null)); + Assert.Equal(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}", JsonSerializer.Serialize(kvp)); + } + + { + KeyValuePair> kvp = new KeyValuePair>("key", new KeyValuePair("key", null)); + Assert.Equal(@"{""Key"":""key"",""Value"":{""Key"":""key"",""Value"":null}}", JsonSerializer.Serialize(kvp)); + } + } + + [Fact] + public static void WriteClassWithNullKeyValuePairValues_NullWrittenAsEmptyObject() + { + var value = new SimpleClassWithKeyValuePairs() + { + KvpWStrVal = new KeyValuePair("key", null), + KvpWObjVal = new KeyValuePair("key", null), + KvpWClassVal = new KeyValuePair("key", null), + KvpWStrKvpVal = new KeyValuePair>("key", new KeyValuePair("key", null)), + KvpWObjKvpVal = new KeyValuePair>("key", new KeyValuePair("key", null)), + KvpWClassKvpVal = new KeyValuePair>("key", new KeyValuePair("key", null)), + }; + + string result = JsonSerializer.Serialize(value); + + // Roundtrip to ensure serialize was correct. + value = JsonSerializer.Deserialize(result); + Assert.Equal("key", value.KvpWStrVal.Key); + Assert.Equal("key", value.KvpWObjVal.Key); + Assert.Equal("key", value.KvpWClassVal.Key); + Assert.Equal("key", value.KvpWStrKvpVal.Key); + Assert.Equal("key", value.KvpWObjKvpVal.Key); + Assert.Equal("key", value.KvpWClassKvpVal.Key); + Assert.Equal("key", value.KvpWStrKvpVal.Value.Key); + Assert.Equal("key", value.KvpWObjKvpVal.Value.Key); + Assert.Equal("key", value.KvpWClassKvpVal.Value.Key); + + Assert.Null(value.KvpWStrVal.Value); + Assert.Null(value.KvpWObjVal.Value); + Assert.Null(value.KvpWClassVal.Value); + Assert.Null(value.KvpWStrKvpVal.Value.Value); + Assert.Null(value.KvpWObjKvpVal.Value.Value); + Assert.Null(value.KvpWClassKvpVal.Value.Value); + } + + [Fact] + public static void HonorNamingPolicy() + { + var kvp = new KeyValuePair("Hello, World!", 1); + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = new LeadingUnderscorePolicy() + }; + + string serialized = JsonSerializer.Serialize(kvp, options); + // We know serializer writes the key first. + Assert.Equal(@"{""_Key"":""Hello, World!"",""_Value"":1}", serialized); + + kvp = JsonSerializer.Deserialize>(serialized, options); + Assert.Equal("Hello, World!", kvp.Key); + Assert.Equal(1, kvp.Value); + } + + [Fact] + public static void HonorNamingPolicy_CaseInsensitive() + { + const string json = @"{""key"":""Hello, World!"",""value"":1}"; + + // Baseline - with case-sensitive matching, the payload doesn't have mapping properties. + Assert.Throws(() => JsonSerializer.Deserialize>(json)); + + // Test - with case-insensitivity on, we have property matches. + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + KeyValuePair kvp = JsonSerializer.Deserialize>(json, options); + Assert.Equal("Hello, World!", kvp.Key); + Assert.Equal(1, kvp.Value); + } + + [Fact] + public static void HonorCLRProperties() + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = new LeadingUnderscorePolicy() // Key -> _Key, Value -> _Value + }; + + // Although policy won't produce this JSON string, the serializer parses the properties + // as "Key" and "Value" are special cased to accomodate content serialized with previous + // versions of the serializer (.NET Core 3.x/System.Text.Json 4.7.x). + string json = @"{""Key"":""Hello, World!"",""Value"":1}"; + KeyValuePair kvp = JsonSerializer.Deserialize>(json, options); + Assert.Equal("Hello, World!", kvp.Key); + Assert.Equal(1, kvp.Value); + + // "Key" and "Value" matching is case sensitive. + json = @"{""key"":""Hello, World!"",""value"":1}"; + Assert.Throws(() => JsonSerializer.Deserialize>(json, options)); + + // "Key" and "Value" matching is case sensitive, even when case sensitivity is on. + // Case sensitivity only applies to the result of converting the CLR property names + // (Key -> _Key, Value -> _Value) with the naming policy. + options = new JsonSerializerOptions + { + PropertyNamingPolicy = new LeadingUnderscorePolicy(), + PropertyNameCaseInsensitive = true + }; + + Assert.Throws(() => JsonSerializer.Deserialize>(json, options)); + } + + private class LeadingUnderscorePolicy : JsonNamingPolicy + { + public override string ConvertName(string name) => "_" + name; + } + + [Fact] + public static void HonorCustomEncoder() + { + var kvp = new KeyValuePair(1, 2); + + JsonNamingPolicy namingPolicy = new TrailingAngleBracketPolicy(); + + // Baseline - properties serialized with default encoder if none specified. + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = namingPolicy, + }; + + Assert.Equal(@"{""Key\u003C"":1,""Value\u003C"":2}", JsonSerializer.Serialize(kvp, options)); + + // Test - serializer honors custom encoder. + options = new JsonSerializerOptions + { + PropertyNamingPolicy = namingPolicy, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + Assert.Equal(@"{""Key<"":1,""Value<"":2}", JsonSerializer.Serialize(kvp, options)); + } + + private class TrailingAngleBracketPolicy : JsonNamingPolicy + { + public override string ConvertName(string name) => name + "<"; + } + + [Theory] + [InlineData(typeof(KeyNameNullPolicy))] + [InlineData(typeof(ValueNameNullPolicy))] + public static void InvalidPropertyNameFail(Type policyType) + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = (JsonNamingPolicy)Activator.CreateInstance(policyType) + }; + + InvalidOperationException ex = Assert.Throws(() => JsonSerializer.Deserialize>("", options)); + string exAsStr = ex.ToString(); + Assert.Contains(policyType.ToString(), exAsStr); + + Assert.Throws(() => JsonSerializer.Serialize(new KeyValuePair("", ""), options)); + } + + private class KeyNameNullPolicy : JsonNamingPolicy + { + public override string ConvertName(string name) => name == "Key" ? null : name; + } + + private class ValueNameNullPolicy : JsonNamingPolicy + { + public override string ConvertName(string name) => name == "Value" ? null : name; + } + + [Theory] + [InlineData("")] + [InlineData("1")] + [InlineData("[")] + [InlineData("}")] + [InlineData("{")] + [InlineData("{}")] + [InlineData("{Key")] + [InlineData("{0")] + [InlineData(@"{""Random"":")] + [InlineData(@"{""Value"":1}")] + [InlineData(@"{""Value"":1,2")] + [InlineData(@"{""Value"":1,""Random"":")] + [InlineData(@"{""Key"":1,""Key"":1}")] + [InlineData(@"{""Key"":1,""Key"":2}")] + [InlineData(@"{""Value"":1,""Value"":1}")] + [InlineData(@"{""Value"":1,""Value"":2}")] + public static void InvalidJsonFail(string json) + { + Assert.Throws(() => JsonSerializer.Deserialize>(json)); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index c55cc68b8500d1..2693078629bfcb 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);$(NetFrameworkCurrent) true @@ -8,8 +8,7 @@ $(DefineConstants);BUILDING_INBOX_LIBRARY - + @@ -45,6 +44,7 @@ + @@ -137,12 +137,10 @@ - + - +