From e98db1609721f7d6c66ffdf16d754acc2a5b917f Mon Sep 17 00:00:00 2001 From: DoctorKrolic <70431552+DoctorKrolic@users.noreply.github.com> Date: Mon, 7 Aug 2023 13:52:08 +0300 Subject: [PATCH] Add `JsonNode.ParseAsync` public API (#90006) * Add `JsonNode.ParseAsync` public API * Regenerate ref assemblies * Change implementation to use unrented document --- .../System.Text.Json/ref/System.Text.Json.cs | 6 +- .../Text/Json/Document/JsonDocument.Parse.cs | 18 ++++++ .../src/System/Text/Json/Nodes/JsonNode.Parse.cs | 31 +++++++++ .../System.Text.Json.Tests/JsonNode/ParseTests.cs | 73 +++++++++++++++++++--- 4 files changed, 115 insertions(+), 13 deletions(-) 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 08b0639..251ede7 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -774,7 +774,7 @@ namespace System.Text.Json.Nodes public static System.Text.Json.Nodes.JsonNode? Parse(System.ReadOnlySpan utf8Json, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions)) { throw null; } public static System.Text.Json.Nodes.JsonNode? Parse([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Json")] string json, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions)) { throw null; } public static System.Text.Json.Nodes.JsonNode? Parse(ref System.Text.Json.Utf8JsonReader reader, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } - + public static System.Threading.Tasks.Task ParseAsync(System.IO.Stream utf8Json, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Creating JsonValue instances with non-primitive types requires generating code at runtime.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Creating JsonValue instances with non-primitive types is not compatible with trimming. It can result in non-primitive types being serialized, which may have their members trimmed.")] public void ReplaceWith(T value) { throw null; } @@ -1217,12 +1217,12 @@ namespace System.Text.Json.Serialization.Metadata public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateIReadOnlyDictionaryInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues collectionInfo) where TCollection : System.Collections.Generic.IReadOnlyDictionary where TKey : notnull { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateISetInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues collectionInfo) where TCollection : System.Collections.Generic.ISet { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateListInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues collectionInfo) where TCollection : System.Collections.Generic.List { throw null; } - public static System.Text.Json.Serialization.Metadata.JsonTypeInfo> CreateMemoryInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues> collectionInfo) { throw null; } + public static System.Text.Json.Serialization.Metadata.JsonTypeInfo> CreateMemoryInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues> collectionInfo) { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateObjectInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonObjectInfoValues objectInfo) where T : notnull { throw null; } public static System.Text.Json.Serialization.Metadata.JsonPropertyInfo CreatePropertyInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues propertyInfo) { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateQueueInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues collectionInfo, System.Action addFunc) where TCollection : System.Collections.IEnumerable { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateQueueInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues collectionInfo) where TCollection : System.Collections.Generic.Queue { throw null; } - public static System.Text.Json.Serialization.Metadata.JsonTypeInfo> CreateReadOnlyMemoryInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues> collectionInfo) { throw null; } + public static System.Text.Json.Serialization.Metadata.JsonTypeInfo> CreateReadOnlyMemoryInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues> collectionInfo) { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateStackInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues collectionInfo, System.Action addFunc) where TCollection : System.Collections.IEnumerable { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateStackInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues collectionInfo) where TCollection : System.Collections.Generic.Stack { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateValueInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.JsonConverter converter) { throw null; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs index f639fcc..e571988 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs @@ -228,6 +228,24 @@ namespace System.Text.Json } } + internal static async Task ParseAsyncCoreUnrented( + Stream utf8Json, + JsonDocumentOptions options = default, + CancellationToken cancellationToken = default) + { + ArraySegment drained = await ReadToEndAsync(utf8Json, cancellationToken).ConfigureAwait(false); + Debug.Assert(drained.Array != null); + + byte[] owned = new byte[drained.Count]; + Buffer.BlockCopy(drained.Array, 0, owned, 0, drained.Count); + + // Holds document content, clear it before returning it. + drained.AsSpan().Clear(); + ArrayPool.Shared.Return(drained.Array); + + return ParseUnrented(owned.AsMemory(), options.GetReaderOptions()); + } + /// /// Parses text representing a single JSON value into a JsonDocument. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.Parse.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.Parse.cs index 750802b..2e3b083 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.Parse.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.Parse.cs @@ -4,6 +4,8 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text.Json.Serialization.Converters; +using System.Threading; +using System.Threading.Tasks; namespace System.Text.Json.Nodes { @@ -126,5 +128,34 @@ namespace System.Text.Json.Nodes JsonElement element = JsonElement.ParseValue(utf8Json, documentOptions); return JsonNodeConverter.Create(element, nodeOptions); } + + /// + /// Parse a as UTF-8 encoded data representing a single JSON value into a + /// . The Stream will be read to completion. + /// + /// JSON text to parse. + /// Options to control the node behavior after parsing. + /// Options to control the document behavior during parsing. + /// The token to monitor for cancellation requests. + /// + /// A to produce a representation of the JSON value. + /// + /// + /// does not represent a valid single JSON value. + /// + public static async Task ParseAsync( + Stream utf8Json, + JsonNodeOptions? nodeOptions = null, + JsonDocumentOptions documentOptions = default, + CancellationToken cancellationToken = default) + { + if (utf8Json is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(utf8Json)); + } + + JsonDocument document = await JsonDocument.ParseAsyncCoreUnrented(utf8Json, documentOptions, cancellationToken).ConfigureAwait(false); + return JsonNodeConverter.Create(document.RootElement, nodeOptions); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParseTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParseTests.cs index 44139de..b8594d8 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParseTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParseTests.cs @@ -4,6 +4,7 @@ using System.IO; using System.Reflection; using System.Text.Json.Serialization.Tests; +using System.Threading.Tasks; using Xunit; namespace System.Text.Json.Nodes.Tests @@ -125,15 +126,16 @@ namespace System.Text.Json.Nodes.Tests } [Fact] - public static void NullReference_Fail() + public static async Task NullReference_Fail() { Assert.Throws(() => JsonSerializer.Deserialize((string)null)); Assert.Throws(() => JsonNode.Parse((string)null)); Assert.Throws(() => JsonNode.Parse((Stream)null)); + await Assert.ThrowsAsync(() => JsonNode.ParseAsync(null)); } [Fact] - public static void NullLiteral() + public static async Task NullLiteral() { Assert.Null(JsonSerializer.Deserialize("null")); Assert.Null(JsonNode.Parse("null")); @@ -141,11 +143,13 @@ namespace System.Text.Json.Nodes.Tests using (MemoryStream stream = new MemoryStream("null"u8.ToArray())) { Assert.Null(JsonNode.Parse(stream)); + stream.Position = 0; + Assert.Null(await JsonNode.ParseAsync(stream)); } } [Fact] - public static void InternalValueFields() + public static async Task InternalValueFields() { // Use reflection to inspect the internal state of the 3 fields that hold values. // There is not another way to verify, and using a debug watch causes nodes to be created. @@ -196,19 +200,51 @@ namespace System.Text.Json.Nodes.Tests Assert.Equal(SimpleTestClass.s_json.StripWhitespace(), actual); } } + + using (MemoryStream stream = new MemoryStream(SimpleTestClass.s_data)) + { + // Only JsonElement is present. + JsonNode node = await JsonNode.ParseAsync(stream); + object jsonDictionary = jsonDictionaryField.GetValue(node); + Assert.Null(jsonDictionary); // Value is null until converted from JsonElement. + Assert.NotNull(elementField.GetValue(node)); + Test(); + + // Cause the single JsonElement to expand into individual JsonElement nodes. + Assert.Equal(1, node.AsObject()["MyInt16"].GetValue()); + Assert.Null(elementField.GetValue(node)); + + jsonDictionary = jsonDictionaryField.GetValue(node); + Assert.NotNull(jsonDictionary); + + Assert.NotNull(listField.GetValue(jsonDictionary)); + Assert.NotNull(dictionaryField.GetValue(jsonDictionary)); // The dictionary threshold was reached. + Test(); + + void Test() + { + string actual = node.ToJsonString(); + + // Replace the escaped "+" sign used with DateTimeOffset. + actual = actual.Replace("\\u002B", "+"); + + Assert.Equal(SimpleTestClass.s_json.StripWhitespace(), actual); + } + } } [Fact] - public static void ReadSimpleObjectWithTrailingTrivia() + public static async Task ReadSimpleObjectWithTrailingTrivia() { byte[] data = Encoding.UTF8.GetBytes(SimpleTestClass.s_json + " /* Multi\r\nLine Comment */\t"); - using (MemoryStream stream = new MemoryStream(data)) + + var options = new JsonDocumentOptions { - var options = new JsonDocumentOptions - { - CommentHandling = JsonCommentHandling.Skip - }; + CommentHandling = JsonCommentHandling.Skip + }; + using (MemoryStream stream = new MemoryStream(data)) + { JsonNode node = JsonNode.Parse(stream, nodeOptions: null, options); string actual = node.ToJsonString(); @@ -217,16 +253,33 @@ namespace System.Text.Json.Nodes.Tests Assert.Equal(SimpleTestClass.s_json.StripWhitespace(), actual); } + + using (MemoryStream stream = new MemoryStream(data)) + { + JsonNode node = await JsonNode.ParseAsync(stream, nodeOptions: null, options); + + string actual = node.ToJsonString(); + // Replace the escaped "+" sign used with DateTimeOffset. + actual = actual.Replace("\\u002B", "+"); + + Assert.Equal(SimpleTestClass.s_json.StripWhitespace(), actual); + } } [Fact] - public static void ReadPrimitives() + public static async Task ReadPrimitives() { using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(@"1"))) { int i = JsonNode.Parse(stream).AsValue().GetValue(); Assert.Equal(1, i); } + + using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(@"1"))) + { + int i = (await JsonNode.ParseAsync(stream)).AsValue().GetValue(); + Assert.Equal(1, i); + } } [Fact] -- 2.7.4