diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs index 1ac5814543c963..ab69c1709c72e9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs @@ -553,34 +553,14 @@ private static string FormatJsonPointer(ReadOnlySpan path) foreach (string segment in path) { - ReadOnlySpan span = segment.AsSpan(); sb.Append('/'); - do - { - // Per RFC 6901 the characters '~' and '/' must be escaped. - int pos = span.IndexOfAny('~', '/'); - if (pos < 0) - { - sb.Append(span); - break; - } - - sb.Append(span.Slice(0, pos)); + // Per RFC 6901 the characters '~' and '/' are escaped as '~0' and '~1'. + string escapedToken = segment.Replace("~", "~0").Replace("/", "~1"); - if (span[pos] == '~') - { - sb.Append("~0"); - } - else - { - Debug.Assert(span[pos] == '/'); - sb.Append("~1"); - } - - span = span.Slice(pos + 1); - } - while (!span.IsEmpty); + // Per RFC 6901 section 6 the JSON Pointer is embedded in a URI fragment, + // so percent-encode any characters that are not valid in a URI fragment. + sb.Append(Uri.EscapeDataString(escapedToken)); } return sb.ToString(); diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs index 065f18e501d5ec..9d5be48d5fe245 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs @@ -1169,6 +1169,29 @@ of the type which points to the first occurrence. */ } """); + yield return new TestData( + Value: new ClassWithPropertyNameRequiringFragmentEncoding { Value = new() }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "hello%20world": { + "type": "object", + "properties": { + "Value" : {"type":"integer"}, + "Next": { + "type": ["object","null"], + "properties": { + "Value" : {"type":"integer"}, + "Next": {"$ref":"#/properties/hello%2520world/properties/Next"} + } + } + } + } + } + } + """); + yield return new TestData( Value: new(value: null), AdditionalValues: [new(true), new(42), new(""), new(new object()), new(Array.Empty())], @@ -1614,6 +1637,12 @@ public class ClassWithJsonPointerEscapablePropertyNames public PocoWithRecursiveMembers Value { get; set; } } + public class ClassWithPropertyNameRequiringFragmentEncoding + { + [JsonPropertyName("hello%20world")] + public PocoWithRecursiveMembers Value { get; set; } + } + public class ClassWithOptionalObjectParameter(object? value = null) { public object? Value { get; } = value; diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs index f4cf65cb00a8b9..9dc8bc2131d925 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs @@ -115,6 +115,7 @@ public sealed partial class JsonSchemaExporterTests_SourceGen() [JsonSerializable(typeof(PocoCombiningPolymorphicTypeAndDerivedTypes))] [JsonSerializable(typeof(ClassWithComponentModelAttributes))] [JsonSerializable(typeof(ClassWithJsonPointerEscapablePropertyNames))] + [JsonSerializable(typeof(ClassWithPropertyNameRequiringFragmentEncoding))] [JsonSerializable(typeof(ClassWithOptionalObjectParameter))] [JsonSerializable(typeof(ClassWithPropertiesUsingCustomConverters))] // Collection types