diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 90575721c507d7..6109fbe2a1f100 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -1311,7 +1311,7 @@ private SourceText GetPropertyNameInitialization(ContextGenerationSpec contextSp foreach (KeyValuePair name_varName_pair in _propertyNames) { - writer.WriteLine($$"""private static readonly {{JsonEncodedTextTypeRef}} {{name_varName_pair.Value}} = {{JsonEncodedTextTypeRef}}.Encode("{{name_varName_pair.Key}}");"""); + writer.WriteLine($$"""private static readonly {{JsonEncodedTextTypeRef}} {{name_varName_pair.Value}} = {{JsonEncodedTextTypeRef}}.Encode({{FormatStringLiteral(name_varName_pair.Key)}});"""); } return CompleteSourceFileAndReturnText(writer); @@ -1343,7 +1343,8 @@ private static string FormatJsonSerializerDefaults(JsonSerializerDefaults defaul private static string GetCreateValueInfoMethodRef(string typeCompilableName) => $"{CreateValueInfoMethodName}<{typeCompilableName}>"; private static string FormatBool(bool value) => value ? "true" : "false"; - private static string FormatStringLiteral(string? value) => value is null ? "null" : $"\"{value}\""; + private static string FormatStringLiteral(string? value) + => value is null ? "null" : SymbolDisplay.FormatLiteral(value, quote: true); /// /// Method used to generate JsonTypeInfo given options instance diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.Escaping.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.Escaping.cs index c4cfe678a5c8e3..a63464d3fedf54 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.Escaping.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.Escaping.cs @@ -53,6 +53,17 @@ public static byte[] EscapeValue( return escapedString; } + public static string GetEscapedPropertyName( + string rawPropertyName, + JavaScriptEncoder? encoder = null) + { + encoder ??= JavaScriptEncoder.Default; + int indexOfFirstCharacterToEncode = JsonWriterHelper.NeedsEscaping(rawPropertyName.AsSpan(), encoder); + return indexOfFirstCharacterToEncode >= 0 + ? encoder.Encode(rawPropertyName) + : rawPropertyName; + } + private static byte[] GetEscapedPropertyNameSection( ReadOnlySpan utf8Value, int firstEscapeIndexVal, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs index 749f5d8d07d886..ece82f0f06d60a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs @@ -206,7 +206,7 @@ internal override void GetPath(ref ValueStringBuilder path, JsonNode? child) if (propertyName.AsSpan().ContainsSpecialCharacters()) { path.Append("['"); - path.Append(propertyName); + path.Append(JsonHelpers.GetEscapedPropertyName(propertyName)); path.Append("']"); } else diff --git a/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs b/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs index 021481ae5a1362..4fb2e2413dae63 100644 --- a/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs @@ -36,6 +36,18 @@ public async Task BuiltInPolicyDeserializeMatch() await DeserializeAndAssert(JsonNamingPolicy.KebabCaseUpper, @"{""MY-INT16"":1}", 1); } + [Theory] + [MemberData(nameof(SuccessDeserialize_TestData))] + public async Task SuccessDeserialize(string valueForDeserialize, ClassWithEscapablePropertyName expectedValue) + { + ClassWithEscapablePropertyName actualValue; + + actualValue = await Serializer.DeserializeWrapper(valueForDeserialize); + + Assert.Equal(expectedValue.MyFunnyProperty, actualValue.MyFunnyProperty); + Assert.Equal(expectedValue.MyFunnyProperty2, actualValue.MyFunnyProperty2); + } + private async Task DeserializeAndAssert(JsonNamingPolicy policy, string json, short expected) { var options = new JsonSerializerOptions { PropertyNamingPolicy = policy }; @@ -495,6 +507,44 @@ public class ClassWithSpecialCharacters public int YiIt_2 { get; set; } } + public static IEnumerable SuccessDeserialize_TestData() + { + yield return new object[] + { + "{\"abc[!@#№$;%:^&?*()-+~`|]'def'\":\"valueFromMyFunnyPropertyTestCase1\"}", + new ClassWithEscapablePropertyName + { + MyFunnyProperty = "valueFromMyFunnyPropertyTestCase1" + }, + }; + yield return new object[] + { + "{\"abc[!@#№$;%:^&?*()-+~`|]'def'\":\"valueFromMyFunnyPropertyTestCase1\", \"withQuote\\\"\": \"valueFromMyFunnyProperty2\"}", + new ClassWithEscapablePropertyName + { + MyFunnyProperty = "valueFromMyFunnyPropertyTestCase1", + MyFunnyProperty2 = "valueFromMyFunnyProperty2" + }, + }; + yield return new object[] + { + "{\"abc[!@#\\u2116$;%:^\\u0026?*()-\\u002B~\\u0060|]\\u0027def\\u0027\":\"valueFromMyFunnyPropertyTestCase2\"}", + new ClassWithEscapablePropertyName + { + MyFunnyProperty = "valueFromMyFunnyPropertyTestCase2" + }, + }; + } + + public class ClassWithEscapablePropertyName + { + [JsonPropertyName("abc[!@#№$;%:^&?*()-+~`|]'def'")] + public string MyFunnyProperty { get; set; } + + [JsonPropertyName("withQuote\"")] + public string MyFunnyProperty2 { get; set; } + } + [Theory] [InlineData(false)] [InlineData(true)] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyNameTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyNameTests.cs index e512451eed72bc..d0cd9a21817f17 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyNameTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyNameTests.cs @@ -18,6 +18,7 @@ public PropertyNameTests_Metadata() [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(ClassWithEscapablePropertyName))] [JsonSerializable(typeof(ClassWithSpecialCharacters))] [JsonSerializable(typeof(ClassWithPropertyNamePermutations))] [JsonSerializable(typeof(ClassWithUnicodeProperty))] @@ -44,6 +45,7 @@ public PropertyNameTests_Default() [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(ClassWithEscapablePropertyName))] [JsonSerializable(typeof(ClassWithSpecialCharacters))] [JsonSerializable(typeof(ClassWithPropertyNamePermutations))] [JsonSerializable(typeof(ClassWithUnicodeProperty))] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParentPathRootTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParentPathRootTests.cs index d9c1a80fa95388..fc8d9e26d74570 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParentPathRootTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParentPathRootTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using Xunit; namespace System.Text.Json.Nodes.Tests @@ -76,6 +77,17 @@ public static void GetPathAndRoot() Assert.Equal("$[0].Child", node[0]["Child"].GetPath()); } + [Theory] + [MemberData(nameof(GetPath_ShouldReturnExpectedValue_TestData))] + public static void GetPath_ShouldReturnExpectedValue(JsonNode jsonNode, string expectedValue) + { + string actualValue; + + actualValue = jsonNode.GetPath(); + + Assert.Equal(expectedValue, actualValue); + } + [Fact] public static void GetPath_SpecialCharacters() { @@ -180,5 +192,49 @@ public static void Parent_Object() parent.Add("MyProp", child); Assert.True(child.Options.Value.PropertyNameCaseInsensitive); } + + public static IEnumerable GetPath_ShouldReturnExpectedValue_TestData() + { + yield return new object[] + { + JsonNode.Parse("""{"$myRoot":{"foo['bar":"baz"}}""")["$myRoot"]["foo['bar"], + "$.$myRoot['foo[\\u0027bar']" + }; + yield return new object[] + { + JsonNode.Parse("""{"$myRoot":{"foo[\"bar":"baz"}}""")["$myRoot"]["foo[\"bar"], + "$.$myRoot['foo[\\u0022bar']" + }; + yield return new object[] + { + JsonNode.Parse("""{"$myRoot":{"foo['b\"ar":"baz"}}""")["$myRoot"]["foo['b\"ar"], + "$.$myRoot['foo[\\u0027b\\u0022ar']" + }; + yield return new object[] + { + JsonNode.Parse("""{"$myRoot": {"myRoot'child": {"myRoot'child'secondLevelChild": "value1"}}}""")["$myRoot"]["myRoot'child"]["myRoot'child'secondLevelChild"], + "$.$myRoot['myRoot\\u0027child']['myRoot\\u0027child\\u0027secondLevelChild']" + }; + yield return new object[] + { + JsonNode.Parse("""{"$myRoot": {"myRoot\"child": {"myRoot\"child\"secondLevelChild": "value1"}}}""")["$myRoot"]["myRoot\"child"]["myRoot\"child\"secondLevelChild"], + "$.$myRoot['myRoot\\u0022child']['myRoot\\u0022child\\u0022secondLevelChild']" + }; + yield return new object[] + { + JsonNode.Parse("""{"$myRoot": {"myRoot\"child": {"myRoot'child\"secondLevelChild": "value1"}}}""")["$myRoot"]["myRoot\"child"]["myRoot'child\"secondLevelChild"], + "$.$myRoot['myRoot\\u0022child']['myRoot\\u0027child\\u0022secondLevelChild']" + }; + yield return new object[] + { + JsonNode.Parse("""{"$myRoot": {"myRoot'child": {"secondLevelChildWithoutEscaping": "value2"}}}""")["$myRoot"]["myRoot'child"]["secondLevelChildWithoutEscaping"], + "$.$myRoot['myRoot\\u0027child'].secondLevelChildWithoutEscaping" + }; + yield return new object[] + { + JsonNode.Parse("""{"$myRoot": {"myRoot\"child": {"secondLevelChildWithoutEscaping": "value2"}}}""")["$myRoot"]["myRoot\"child"]["secondLevelChildWithoutEscaping"], + "$.$myRoot['myRoot\\u0022child'].secondLevelChildWithoutEscaping" + }; + } } }