Adds de/serialization support for JsonDocument (#34537)
authorMarcus Turewicz <24448509+marcusturewicz@users.noreply.github.com>
Mon, 6 Apr 2020 23:56:54 +0000 (09:56 +1000)
committerGitHub <noreply@github.com>
Mon, 6 Apr 2020 23:56:54 +0000 (16:56 -0700)
* Adds deserialization support for JsonDocument

This change adds support to `System.Text.Json` for deserializing `JsonDocument`.

Specifically, an internal converter `JsonDocumentConverter` is added to the default converter dictionary.

I have created a basic test, but I feel more could be done here, and it may not be in the right file/class - some guidance would be helpful here.

Fixes #1573

* Adds JsonDocumentTests

* Dispose JsonDocument

src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
src/libraries/System.Text.Json/tests/Serialization/JsonDocumentTests.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/Serialization/TestData.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj

index 94c7b47..5ba0ea9 100644 (file)
     <Compile Include="System\Text\Json\Serialization\Converters\Value\Int32Converter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Value\Int64Converter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Value\JsonElementConverter.cs" />
+    <Compile Include="System\Text\Json\Serialization\Converters\Value\JsonDocumentConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Value\KeyValuePairConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Value\KeyValuePairConverterFactory.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Value\NullableConverter.cs" />
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs
new file mode 100644 (file)
index 0000000..f30fe79
--- /dev/null
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System.Text.Json.Serialization.Converters
+{
+    internal sealed class JsonDocumentConverter : JsonConverter<JsonDocument>
+    {
+        public override JsonDocument Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            return JsonDocument.ParseValue(ref reader);
+        }
+
+        public override void Write(Utf8JsonWriter writer, JsonDocument value, JsonSerializerOptions options)
+        {
+            value.WriteTo(writer);
+        }
+    }
+}
index 8a241a1..1d757ef 100644 (file)
@@ -37,7 +37,7 @@ namespace System.Text.Json
 
         private static Dictionary<Type, JsonConverter> GetDefaultSimpleConverters()
         {
-            const int NumberOfSimpleConverters = 22;
+            const int NumberOfSimpleConverters = 23;
             var converters = new Dictionary<Type, JsonConverter>(NumberOfSimpleConverters);
 
             // Use a dictionary for simple converters.
@@ -55,6 +55,7 @@ namespace System.Text.Json
             Add(new Int32Converter());
             Add(new Int64Converter());
             Add(new JsonElementConverter());
+            Add(new JsonDocumentConverter());
             Add(new ObjectConverter());
             Add(new SByteConverter());
             Add(new SingleConverter());
diff --git a/src/libraries/System.Text.Json/tests/Serialization/JsonDocumentTests.cs b/src/libraries/System.Text.Json/tests/Serialization/JsonDocumentTests.cs
new file mode 100644 (file)
index 0000000..3e32329
--- /dev/null
@@ -0,0 +1,179 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+    public class JsonDocumentTests
+    {
+        [Fact]
+        public void SerializeJsonDocument()
+        {
+            using JsonDocumentClass obj = new JsonDocumentClass();
+            obj.Document = JsonSerializer.Deserialize<JsonDocument>(JsonDocumentClass.s_json);
+            obj.Verify();
+            string reserialized = JsonSerializer.Serialize(obj.Document);
+
+            // Properties in the exported json will be in the order that they were reflected, doing a quick check to see that
+            // we end up with the same length (i.e. same amount of data) to start.
+            Assert.Equal(JsonDocumentClass.s_json.StripWhitespace().Length, reserialized.Length);
+
+            // Shoving it back through the parser should validate round tripping.
+            obj.Document = JsonSerializer.Deserialize<JsonDocument>(reserialized);
+            obj.Verify();
+        }
+
+        public class JsonDocumentClass : ITestClass, IDisposable
+        {
+            public JsonDocument Document { get; set; }
+
+            public static readonly string s_json =
+                @"{" +
+                    @"""Number"" : 1," +
+                    @"""True"" : true," +
+                    @"""False"" : false," +
+                    @"""String"" : ""Hello""," +
+                    @"""Array"" : [2, false, true, ""Goodbye""]," +
+                    @"""Object"" : {}," +
+                    @"""Null"" : null" +
+                @"}";
+
+            public readonly byte[] s_data = Encoding.UTF8.GetBytes(s_json);
+
+            public void Initialize()
+            {
+                Document = JsonDocument.Parse(s_json);
+            }
+
+            public void Verify()
+            {
+                JsonElement number = Document.RootElement.GetProperty("Number");
+                JsonElement trueBool = Document.RootElement.GetProperty("True");
+                JsonElement falseBool = Document.RootElement.GetProperty("False");
+                JsonElement stringType = Document.RootElement.GetProperty("String");
+                JsonElement arrayType = Document.RootElement.GetProperty("Array");
+                JsonElement objectType = Document.RootElement.GetProperty("Object");
+                JsonElement nullType = Document.RootElement.GetProperty("Null");
+
+                Assert.Equal(JsonValueKind.Number, number.ValueKind);
+                Assert.Equal("1", number.ToString());
+                Assert.Equal(JsonValueKind.True, trueBool.ValueKind);
+                Assert.Equal("True", true.ToString());
+                Assert.Equal(JsonValueKind.False, falseBool.ValueKind);
+                Assert.Equal("False", false.ToString());
+                Assert.Equal(JsonValueKind.String, stringType.ValueKind);
+                Assert.Equal("Hello", stringType.ToString());
+                Assert.Equal(JsonValueKind.Array, arrayType.ValueKind);
+                JsonElement[] elements = arrayType.EnumerateArray().ToArray();
+                Assert.Equal(JsonValueKind.Number, elements[0].ValueKind);
+                Assert.Equal("2", elements[0].ToString());
+                Assert.Equal(JsonValueKind.False, elements[1].ValueKind);
+                Assert.Equal("False", elements[1].ToString());
+                Assert.Equal(JsonValueKind.True, elements[2].ValueKind);
+                Assert.Equal("True", elements[2].ToString());
+                Assert.Equal(JsonValueKind.String, elements[3].ValueKind);
+                Assert.Equal("Goodbye", elements[3].ToString());
+                Assert.Equal(JsonValueKind.Object, objectType.ValueKind);
+                Assert.Equal("{}", objectType.ToString());
+                Assert.Equal(JsonValueKind.Null, nullType.ValueKind);
+                Assert.Equal("", nullType.ToString()); // JsonElement returns empty string for null.
+            }
+
+            public void Dispose()
+            {
+                Document.Dispose();
+            }
+        }
+
+        [Fact]
+        public void SerializeJsonElementArray()
+        {
+            using JsonDocumentArrayClass obj = new JsonDocumentArrayClass();
+            obj.Document = JsonSerializer.Deserialize<JsonDocument>(JsonDocumentArrayClass.s_json);
+            obj.Verify();
+            string reserialized = JsonSerializer.Serialize(obj.Document);
+
+            // Properties in the exported json will be in the order that they were reflected, doing a quick check to see that
+            // we end up with the same length (i.e. same amount of data) to start.
+            Assert.Equal(JsonDocumentArrayClass.s_json.StripWhitespace().Length, reserialized.Length);
+
+            // Shoving it back through the parser should validate round tripping.
+            obj.Document = JsonSerializer.Deserialize<JsonDocument>(reserialized);
+            obj.Verify();
+        }
+
+        public class JsonDocumentArrayClass : ITestClass, IDisposable
+        {
+            public JsonDocument Document { get; set; }
+
+            public static readonly string s_json =
+                @"{" +
+                    @"""Array"" : [" +
+                        @"1, " +
+                        @"true, " +
+                        @"false, " +
+                        @"""Hello""," +
+                        @"[2, false, true, ""Goodbye""]," +
+                        @"{}" +
+                    @"]" +
+                @"}";
+
+            public static readonly byte[] s_data = Encoding.UTF8.GetBytes(s_json);
+
+            public void Initialize()
+            {
+                Document = JsonDocument.Parse(s_json);
+            }
+
+            public void Verify()
+            {
+                JsonElement[] array = Document.RootElement.GetProperty("Array").EnumerateArray().ToArray();
+
+                Assert.Equal(JsonValueKind.Number, array[0].ValueKind);
+                Assert.Equal("1", array[0].ToString());
+                Assert.Equal(JsonValueKind.True, array[1].ValueKind);
+                Assert.Equal("True", array[1].ToString());
+                Assert.Equal(JsonValueKind.False, array[2].ValueKind);
+                Assert.Equal("False", array[2].ToString());
+                Assert.Equal(JsonValueKind.String, array[3].ValueKind);
+                Assert.Equal("Hello", array[3].ToString());
+            }
+
+            public void Dispose()
+            {
+                Document.Dispose();
+            }
+        }
+
+        [Theory,
+            InlineData(5),
+            InlineData(10),
+            InlineData(20),
+            InlineData(1024)]
+        public void ReadJsonDocumentFromStream(int defaultBufferSize)
+        {
+            // Streams need to read ahead when they hit objects or arrays that are assigned to JsonElement or object.
+
+            byte[] data = Encoding.UTF8.GetBytes(@"{""Data"":[1,true,{""City"":""MyCity""},null,""foo""]}");
+            MemoryStream stream = new MemoryStream(data);
+            JsonDocument obj = JsonSerializer.DeserializeAsync<JsonDocument>(stream, new JsonSerializerOptions { DefaultBufferSize = defaultBufferSize }).Result;
+
+            data = Encoding.UTF8.GetBytes(@"[1,true,{""City"":""MyCity""},null,""foo""]");
+            stream = new MemoryStream(data);
+            obj = JsonSerializer.DeserializeAsync<JsonDocument>(stream, new JsonSerializerOptions { DefaultBufferSize = defaultBufferSize }).Result;
+
+            // Ensure we fail with incomplete data
+            data = Encoding.UTF8.GetBytes(@"{""Data"":[1,true,{""City"":""MyCity""},null,""foo""]");
+            stream = new MemoryStream(data);
+            Assert.Throws<JsonException>(() => JsonSerializer.DeserializeAsync<JsonDocument>(stream, new JsonSerializerOptions { DefaultBufferSize = defaultBufferSize }).Result);
+
+            data = Encoding.UTF8.GetBytes(@"[1,true,{""City"":""MyCity""},null,""foo""");
+            stream = new MemoryStream(data);
+            Assert.Throws<JsonException>(() => JsonSerializer.DeserializeAsync<JsonDocument>(stream, new JsonSerializerOptions { DefaultBufferSize = defaultBufferSize }).Result);
+        }
+    }
+}
index 8bd6cb7..68b9b3c 100644 (file)
@@ -91,6 +91,8 @@ namespace System.Text.Json.Serialization.Tests
                 yield return new object[] { new TestClassWithObjectImmutableTypes() };
                 yield return new object[] { new JsonElementTests.JsonElementClass() };
                 yield return new object[] { new JsonElementTests.JsonElementArrayClass() };
+                yield return new object[] { new JsonDocumentTests.JsonDocumentClass() };
+                yield return new object[] { new JsonDocumentTests.JsonDocumentArrayClass() };
                 yield return new object[] { new ClassWithComplexObjects() };
             }
         }
index a0b35dc..1556634 100644 (file)
@@ -72,6 +72,7 @@
     <Compile Include="Serialization\ExceptionTests.cs" />
     <Compile Include="Serialization\ExtensionDataTests.cs" />
     <Compile Include="Serialization\JsonElementTests.cs" />
+    <Compile Include="Serialization\JsonDocumentTests.cs" />
     <Compile Include="Serialization\Null.ReadTests.cs" />
     <Compile Include="Serialization\Null.WriteTests.cs" />
     <Compile Include="Serialization\NullableTests.cs" />
   <ItemGroup>
     <PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>