Add ReadOnlySpan<char> overloads to JsonSerializer.Deserialize (#41957)
authorHuo Yaoyuan <huoyaoyuan@hotmail.com>
Wed, 14 Oct 2020 15:19:24 +0000 (23:19 +0800)
committerGitHub <noreply@github.com>
Wed, 14 Oct 2020 15:19:24 +0000 (08:19 -0700)
* Add char span overloads to JsonSerializer.Deserialize.

* Add new methods to ref source.

* Fix ambiguous call in test.

* Fix documentation issue.

* Guard and test for empty span.

* Add span in deserialization test wrapper.

* Apply doc suggestions

Co-authored-by: Layomi Akinrinade <layomia@gmail.com>
* Treat default span as empty, and cover returnType in test.

* Apply doc suggestions

Co-authored-by: Ahson Khan <ahkha@microsoft.com>
* Add exception doc for null returnType.

Co-authored-by: Layomi Akinrinade <layomia@gmail.com>
Co-authored-by: Ahson Khan <ahkha@microsoft.com>
src/libraries/System.Text.Json/ref/System.Text.Json.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs
src/libraries/System.Text.Json/tests/Serialization/ConstructorTests/ConstructorTests.ParameterMatching.cs
src/libraries/System.Text.Json/tests/Serialization/DeserializationWrapper.cs
src/libraries/System.Text.Json/tests/Serialization/ReadValueTests.cs

index f8fce13..14b3955 100644 (file)
@@ -185,12 +185,14 @@ namespace System.Text.Json
     public static partial class JsonSerializer
     {
         public static object? Deserialize(System.ReadOnlySpan<byte> utf8Json, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] System.Type returnType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
+        public static object? Deserialize(System.ReadOnlySpan<char> json, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] System.Type returnType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
         public static object? Deserialize(string json, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] System.Type returnType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
         public static object? Deserialize(ref System.Text.Json.Utf8JsonReader reader, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] System.Type returnType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
         public static System.Threading.Tasks.ValueTask<object?> DeserializeAsync(System.IO.Stream utf8Json, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] System.Type returnType, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
         public static System.Threading.Tasks.ValueTask<TValue?> DeserializeAsync<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(System.IO.Stream utf8Json, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
         public static TValue? Deserialize<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(System.ReadOnlySpan<byte> utf8Json, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
         public static TValue? Deserialize<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string json, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
+        public static TValue? Deserialize<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(System.ReadOnlySpan<char> json, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
         public static TValue? Deserialize<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(ref System.Text.Json.Utf8JsonReader reader, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
         public static string Serialize(object? value, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] System.Type inputType, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
         public static void Serialize(System.Text.Json.Utf8JsonWriter writer, object? value, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] System.Type inputType, System.Text.Json.JsonSerializerOptions? options = null) { }
index af1f35d..2fe4f81 100644 (file)
@@ -23,10 +23,15 @@ namespace System.Text.Json
         /// <paramref name="json"/> is <see langword="null"/>.
         /// </exception>
         /// <exception cref="JsonException">
-        /// Thrown when the JSON is invalid,
-        /// <typeparamref name="TValue"/> is not compatible with the JSON,
-        /// or when there is remaining data in the Stream.
-        /// </exception>
+        /// The JSON is invalid.
+        ///
+        /// -or-
+        ///
+        /// <typeparamref name="TValue" /> is not compatible with the JSON.
+        ///
+        /// -or-
+        ///
+        /// There is remaining data in the string beyond a single JSON value.</exception>
         /// <exception cref="NotSupportedException">
         /// There is no compatible <see cref="System.Text.Json.Serialization.JsonConverter"/>
         /// for <typeparamref name="TValue"/> or its serializable members.
@@ -41,6 +46,36 @@ namespace System.Text.Json
                 throw new ArgumentNullException(nameof(json));
             }
 
+            return Deserialize<TValue>(json.AsSpan(), typeof(TValue), options);
+        }
+
+        /// <summary>
+        /// Parses the text representing a single JSON value into an instance of the type specified by a generic type parameter.
+        /// </summary>
+        /// <returns>A <typeparamref name="TValue"/> representation of the JSON value.</returns>
+        /// <param name="json">The JSON text to parse.</param>
+        /// <param name="options">Options to control the behavior during parsing.</param>
+        /// <exception cref="JsonException">
+        /// The JSON is invalid.
+        ///
+        /// -or-
+        ///
+        /// <typeparamref name="TValue" /> is not compatible with the JSON.
+        ///
+        /// -or-
+        ///
+        /// There is remaining data in the span beyond a single JSON value.</exception>
+        /// <exception cref="NotSupportedException">
+        /// There is no compatible <see cref="System.Text.Json.Serialization.JsonConverter"/>
+        /// for <typeparamref name="TValue"/> or its serializable members.
+        /// </exception>
+        /// <remarks>Using a UTF-16 span is not as efficient as using the
+        /// UTF-8 methods since the implementation natively uses UTF-8.
+        /// </remarks>
+        public static TValue? Deserialize<[DynamicallyAccessedMembers(MembersAccessedOnRead)] TValue>(ReadOnlySpan<char> json, JsonSerializerOptions? options = null)
+        {
+            // default/null span is treated as empty
+
             return Deserialize<TValue>(json, typeof(TValue), options);
         }
 
@@ -55,10 +90,15 @@ namespace System.Text.Json
         /// <paramref name="json"/> or <paramref name="returnType"/> is <see langword="null"/>.
         /// </exception>
         /// <exception cref="JsonException">
-        /// Thrown when the JSON is invalid,
-        /// the <paramref name="returnType"/> is not compatible with the JSON,
-        /// or when there is remaining data in the Stream.
-        /// </exception>
+        /// The JSON is invalid.
+        ///
+        /// -or-
+        ///
+        /// <paramref name="returnType"/> is not compatible with the JSON.
+        ///
+        /// -or-
+        ///
+        /// There is remaining data in the string beyond a single JSON value.</exception>
         /// <exception cref="NotSupportedException">
         /// There is no compatible <see cref="System.Text.Json.Serialization.JsonConverter"/>
         /// for <paramref name="returnType"/> or its serializable members.
@@ -78,12 +118,53 @@ namespace System.Text.Json
                 throw new ArgumentNullException(nameof(returnType));
             }
 
+            object? value = Deserialize<object?>(json.AsSpan(), returnType, options)!;
+
+            return value;
+        }
+
+        /// <summary>
+        /// Parse the text representing a single JSON value into an instance of a specified type.
+        /// </summary>
+        /// <returns>A <paramref name="returnType"/> representation of the JSON value.</returns>
+        /// <param name="json">The JSON text to parse.</param>
+        /// <param name="returnType">The type of the object to convert to and return.</param>
+        /// <param name="options">Options to control the behavior during parsing.</param>
+        /// <exception cref="System.ArgumentNullException">
+        /// <paramref name="returnType"/> is <see langword="null"/>.
+        /// </exception>
+        /// <exception cref="JsonException">
+        /// The JSON is invalid.
+        ///
+        /// -or-
+        ///
+        /// <paramref name="returnType"/> is not compatible with the JSON.
+        ///
+        /// -or-
+        ///
+        /// There is remaining data in the span beyond a single JSON value.</exception>
+        /// <exception cref="NotSupportedException">
+        /// There is no compatible <see cref="System.Text.Json.Serialization.JsonConverter"/>
+        /// for <paramref name="returnType"/> or its serializable members.
+        /// </exception>
+        /// <remarks>Using a UTF-16 span is not as efficient as using the
+        /// UTF-8 methods since the implementation natively uses UTF-8.
+        /// </remarks>
+        public static object? Deserialize(ReadOnlySpan<char> json, [DynamicallyAccessedMembers(MembersAccessedOnRead)] Type returnType, JsonSerializerOptions? options = null)
+        {
+            // default/null span is treated as empty
+
+            if (returnType == null)
+            {
+                throw new ArgumentNullException(nameof(returnType));
+            }
+
             object? value = Deserialize<object?>(json, returnType, options)!;
 
             return value;
         }
 
-        private static TValue? Deserialize<TValue>(string json, Type returnType, JsonSerializerOptions? options)
+        private static TValue? Deserialize<TValue>(ReadOnlySpan<char> json, Type returnType, JsonSerializerOptions? options)
         {
             const long ArrayPoolMaxSizeBeforeUsingNormalAlloc = 1024 * 1024;
 
@@ -100,11 +181,11 @@ namespace System.Text.Json
                 tempArray = ArrayPool<byte>.Shared.Rent(json.Length * JsonConstants.MaxExpansionFactorWhileTranscoding) :
                 // Use a normal alloc since the pool would create a normal alloc anyway based on the threshold (per current implementation)
                 // and by using a normal alloc we can avoid the Clear().
-                new byte[JsonReaderHelper.GetUtf8ByteCount(json.AsSpan())];
+                new byte[JsonReaderHelper.GetUtf8ByteCount(json)];
 
             try
             {
-                int actualByteCount = JsonReaderHelper.GetUtf8FromText(json.AsSpan(), utf8);
+                int actualByteCount = JsonReaderHelper.GetUtf8FromText(json, utf8);
                 utf8 = utf8.Slice(0, actualByteCount);
 
                 var readerState = new JsonReaderState(options.GetReaderOptions());
index 91c224d..4b28b57 100644 (file)
@@ -18,6 +18,11 @@ namespace System.Text.Json.Serialization.Tests
         public ConstructorTests_Stream() : base(DeserializationWrapper.StreamDeserializer) { }
     }
 
+    public class ConstructorTests_Span : ConstructorTests
+    {
+        public ConstructorTests_Span() : base(DeserializationWrapper.SpanDeserializer) { }
+    }
+
     public abstract partial class ConstructorTests
     {
         private DeserializationWrapper Serializer { get; }
index b20b6f5..1c84c0b 100644 (file)
@@ -15,6 +15,7 @@ namespace System.Text.Json.Serialization.Tests
 
         public static DeserializationWrapper StringDeserializer => new StringDeserializerWrapper();
         public static DeserializationWrapper StreamDeserializer => new StreamDeserializerWrapper();
+        public static DeserializationWrapper SpanDeserializer => new SpanDesearializationWrapper();
 
         protected internal abstract Task<T> DeserializeWrapper<T>(string json, JsonSerializerOptions options = null);
 
@@ -61,5 +62,18 @@ namespace System.Text.Json.Serialization.Tests
                 }
             }
         }
+
+        private class SpanDesearializationWrapper : DeserializationWrapper
+        {
+            protected internal override Task<T> DeserializeWrapper<T>(string json, JsonSerializerOptions options = null)
+            {
+                return Task.FromResult(JsonSerializer.Deserialize<T>(json.AsSpan(), options));
+            }
+
+            protected internal override Task<object> DeserializeWrapper(string json, Type type, JsonSerializerOptions options = null)
+            {
+                return Task.FromResult(JsonSerializer.Deserialize(json.AsSpan(), type, options));
+            }
+        }
     }
 }
index 16d7f1e..c29c401 100644 (file)
@@ -23,6 +23,9 @@ namespace System.Text.Json.Serialization.Tests
             ex = Assert.Throws<ArgumentNullException>(() => JsonSerializer.Deserialize("", returnType: null));
             Assert.Contains("returnType", ex.ToString());
 
+            ex = Assert.Throws<ArgumentNullException>(() => JsonSerializer.Deserialize(new char[] { '1' }, returnType: null));
+            Assert.Contains("returnType", ex.ToString());
+
             ex = Assert.Throws<ArgumentNullException>(() => JsonSerializer.Deserialize(new byte[] { 1 }, returnType: null));
             Assert.Contains("returnType", ex.ToString());
 
@@ -33,7 +36,7 @@ namespace System.Text.Json.Serialization.Tests
         [Fact]
         public static void NullJsonThrows()
         {
-            ArgumentNullException ex = Assert.Throws<ArgumentNullException>(() => JsonSerializer.Deserialize(json: null, returnType: typeof(string)));
+            ArgumentNullException ex = Assert.Throws<ArgumentNullException>(() => JsonSerializer.Deserialize(json: (string)null, returnType: typeof(string)));
             Assert.Contains("json", ex.ToString());
 
             ex = Assert.Throws<ArgumentNullException>(() => JsonSerializer.DeserializeAsync(utf8Json: null, returnType: null));