Skip to content

Commit

Permalink
Add JsonMarshal.GetRawUtf8Value (#104595)
Browse files Browse the repository at this point in the history
* Add JsonMarshal.TryGetRawValue

* Add escaping test.

* Apply API review suggestions.

* Address feedback
  • Loading branch information
eiriktsarpalis authored Jul 10, 2024
1 parent a48a39f commit 635f9af
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 0 deletions.
7 changes: 7 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> GetRawUtf8Value(System.Text.Json.JsonElement element) { throw null; }
}
}
namespace System.Text.Json
{
public enum JsonCommentHandling : byte
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="..\Common\JsonUnknownTypeHandling.cs" Link="Common\System\Text\Json\Serialization\JsonUnknownTypeHandling.cs" />
<Compile Include="..\Common\ReflectionExtensions.cs" Link="Common\System\Text\Json\Serialization\ReflectionExtensions.cs" />
<Compile Include="..\Common\ThrowHelper.cs" Link="Common\System\Text\Json\ThrowHelper.cs" />
<Compile Include="System\Runtime\InteropServices\JsonMarshal.cs" />
<Compile Include="System\Text\Json\AppContextSwitchHelper.cs" />
<Compile Include="System\Text\Json\BitStack.cs" />
<Compile Include="System\Text\Json\Document\JsonDocument.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// An unsafe class that provides a set of methods to access the underlying data representations of JSON types.
/// </summary>
public static class JsonMarshal
{
/// <summary>
/// Gets a <see cref="ReadOnlySpan{T}"/> view over the raw JSON data of the given <see cref="JsonElement"/>.
/// </summary>
/// <param name="element">The JSON element from which to extract the span.</param>
/// <returns>The span containing the raw JSON data of<paramref name="element"/>.</returns>
/// <exception cref="ObjectDisposedException">The underlying <see cref="JsonDocument"/> has been disposed.</exception>
/// <remarks>
/// While the method itself does check for disposal of the underlying <see cref="JsonDocument"/>,
/// 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.
/// </remarks>
public static ReadOnlySpan<byte> GetRawUtf8Value(JsonElement element)
{
return element.GetRawValue().Span;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Xunit;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace System.Text.Json.Tests
{
Expand Down Expand Up @@ -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<byte> 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<byte> 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<ObjectDisposedException>(() => JsonMarshal.GetRawUtf8Value(element));
}
}
}

0 comments on commit 635f9af

Please sign in to comment.