diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 27f776a8d3c6fe..2b11ad44e43816 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -4,6 +4,13 @@ // Changes to this file must follow the https://aka.ms/api-review process. // ------------------------------------------------------------------------------ +namespace System.Runtime.InteropServices +{ + public static partial class JsonMarshal + { + public static System.ReadOnlySpan GetRawUtf8Value(System.Text.Json.JsonElement element) { throw null; } + } +} namespace System.Text.Json { public enum JsonCommentHandling : byte diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index a38e38ceb6a93c..c01f6ad2342f84 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -50,6 +50,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + diff --git a/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs b/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs new file mode 100644 index 00000000000000..5c8c43805c91a4 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Runtime/InteropServices/JsonMarshal.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace System.Runtime.InteropServices +{ + /// + /// An unsafe class that provides a set of methods to access the underlying data representations of JSON types. + /// + public static class JsonMarshal + { + /// + /// Gets a view over the raw JSON data of the given . + /// + /// The JSON element from which to extract the span. + /// The span containing the raw JSON data of. + /// The underlying has been disposed. + /// + /// While the method itself does check for disposal of the underlying , + /// it is possible that it could be disposed after the method returns, which would result in + /// the span pointing to a buffer that has been returned to the shared pool. Callers should take + /// extra care to make sure that such a scenario isn't possible to avoid potential data corruption. + /// + public static ReadOnlySpan GetRawUtf8Value(JsonElement element) + { + return element.GetRawValue().Span; + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs index 15a399c01fbada..017b1422ab7d74 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonElementParseTests.cs @@ -3,6 +3,7 @@ using Xunit; using System.Collections.Generic; +using System.Runtime.InteropServices; namespace System.Text.Json.Tests { @@ -225,5 +226,134 @@ public static void TryParseValueInvalidDataFail(string json) Assert.Equal(0, reader.BytesConsumed); } + + [Theory] + [InlineData("null")] + [InlineData("\r\n null ")] + [InlineData("false")] + [InlineData("true ")] + [InlineData(" 42.0 ")] + [InlineData(" \"str\" \r\n")] + [InlineData(" \"string with escaping: \\u0041\\u0042\\u0043\" \r\n")] + [InlineData(" [ ]")] + [InlineData(" [null, true, 42.0, \"str\", [], {}, ]")] + [InlineData(" { } ")] + [InlineData(""" + + { + /* I am a comment */ + "key1" : 1, + "key2" : null, + "key3" : true, + } + + """)] + public static void JsonMarshal_GetRawUtf8Value_RootValue_ReturnsFullValue(string json) + { + JsonDocumentOptions options = new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }; + using JsonDocument jDoc = JsonDocument.Parse(json, options); + JsonElement element = jDoc.RootElement; + + ReadOnlySpan rawValue = JsonMarshal.GetRawUtf8Value(element); + Assert.Equal(json.Trim(), Encoding.UTF8.GetString(rawValue.ToArray())); + } + + [Fact] + public static void JsonMarshal_GetRawUtf8Value_NestedValues_ReturnsExpectedValue() + { + const string json = """ + { + "date": "2021-06-01T00:00:00Z", + "temperatureC": 25, + "summary": "Hot", + + /* The next property is a JSON object */ + + "nested": { + /* This is a nested JSON object */ + + "nestedDate": "2021-06-01T00:00:00Z", + "nestedTemperatureC": 25, + "nestedSummary": "Hot" + }, + + /* The next property is a JSON array */ + + "nestedArray": [ + /* This is a JSON array */ + { + "nestedDate": "2021-06-01T00:00:00Z", + "nestedTemperatureC": 25, + "nestedSummary": "Hot" + }, + ] + } + """; + + JsonDocumentOptions options = new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }; + using JsonDocument jDoc = JsonDocument.Parse(json, options); + JsonElement element = jDoc.RootElement; + + AssertGetRawValue(json, element); + AssertGetRawValue("\"2021-06-01T00:00:00Z\"", element.GetProperty("date")); + AssertGetRawValue("25", element.GetProperty("temperatureC")); + AssertGetRawValue("\"Hot\"", element.GetProperty("summary")); + + JsonElement nested = element.GetProperty("nested"); + AssertGetRawValue(""" + { + /* This is a nested JSON object */ + + "nestedDate": "2021-06-01T00:00:00Z", + "nestedTemperatureC": 25, + "nestedSummary": "Hot" + } + """, nested); + + AssertGetRawValue("\"2021-06-01T00:00:00Z\"", nested.GetProperty("nestedDate")); + AssertGetRawValue("25", nested.GetProperty("nestedTemperatureC")); + AssertGetRawValue("\"Hot\"", nested.GetProperty("nestedSummary")); + + JsonElement nestedArray = element.GetProperty("nestedArray"); + AssertGetRawValue(""" + [ + /* This is a JSON array */ + { + "nestedDate": "2021-06-01T00:00:00Z", + "nestedTemperatureC": 25, + "nestedSummary": "Hot" + }, + ] + """, nestedArray); + + JsonElement nestedArrayElement = nestedArray[0]; + AssertGetRawValue(""" + { + "nestedDate": "2021-06-01T00:00:00Z", + "nestedTemperatureC": 25, + "nestedSummary": "Hot" + } + """, nestedArrayElement); + + AssertGetRawValue("\"2021-06-01T00:00:00Z\"", nestedArrayElement.GetProperty("nestedDate")); + AssertGetRawValue("25", nestedArrayElement.GetProperty("nestedTemperatureC")); + AssertGetRawValue("\"Hot\"", nestedArrayElement.GetProperty("nestedSummary")); + + static void AssertGetRawValue(string expectedJson, JsonElement element) + { + ReadOnlySpan rawValue = JsonMarshal.GetRawUtf8Value(element); + Assert.Equal(expectedJson.Trim(), Encoding.UTF8.GetString(rawValue.ToArray())); + } + } + + [Fact] + public static void JsonMarshal_GetRawUtf8Value_DisposedDocument_ThrowsObjectDisposedException() + { + JsonDocument jDoc = JsonDocument.Parse("{}"); + JsonElement element = jDoc.RootElement; + jDoc.Dispose(); + + Assert.Throws(() => JsonMarshal.GetRawUtf8Value(element)); + } } }