From 5dcf3decb92fb77dc57f34c5f2926291759844ae Mon Sep 17 00:00:00 2001 From: Marcus Turewicz <24448509+marcusturewicz@users.noreply.github.com> Date: Tue, 7 Apr 2020 09:56:54 +1000 Subject: [PATCH] Adds de/serialization support for JsonDocument (#34537) * 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/System.Text.Json.csproj | 1 + .../Converters/Value/JsonDocumentConverter.cs | 19 ++ .../JsonSerializerOptions.Converters.cs | 3 +- .../tests/Serialization/JsonDocumentTests.cs | 179 ++++++++++++++++++ .../tests/Serialization/TestData.cs | 2 + .../tests/System.Text.Json.Tests.csproj | 3 +- 6 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs create mode 100644 src/libraries/System.Text.Json/tests/Serialization/JsonDocumentTests.cs 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 94c7b47f1b0c6..5ba0ea99214c3 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -101,6 +101,7 @@ + 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 index 0000000000000..f30fe79ca7918 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs @@ -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 + { + 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); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index 8a241a13c9fd9..1d757ef80ce6b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -37,7 +37,7 @@ public sealed partial class JsonSerializerOptions private static Dictionary GetDefaultSimpleConverters() { - const int NumberOfSimpleConverters = 22; + const int NumberOfSimpleConverters = 23; var converters = new Dictionary(NumberOfSimpleConverters); // Use a dictionary for simple converters. @@ -55,6 +55,7 @@ private static Dictionary GetDefaultSimpleConverters() 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 index 0000000000000..3e32329f8c65c --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/JsonDocumentTests.cs @@ -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(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(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(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(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(stream, new JsonSerializerOptions { DefaultBufferSize = defaultBufferSize }).Result; + + data = Encoding.UTF8.GetBytes(@"[1,true,{""City"":""MyCity""},null,""foo""]"); + stream = new MemoryStream(data); + obj = JsonSerializer.DeserializeAsync(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(() => JsonSerializer.DeserializeAsync(stream, new JsonSerializerOptions { DefaultBufferSize = defaultBufferSize }).Result); + + data = Encoding.UTF8.GetBytes(@"[1,true,{""City"":""MyCity""},null,""foo"""); + stream = new MemoryStream(data); + Assert.Throws(() => JsonSerializer.DeserializeAsync(stream, new JsonSerializerOptions { DefaultBufferSize = defaultBufferSize }).Result); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/TestData.cs b/src/libraries/System.Text.Json/tests/Serialization/TestData.cs index 8bd6cb73cf051..68b9b3ce93d12 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/TestData.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/TestData.cs @@ -91,6 +91,8 @@ public static IEnumerable WriteSuccessCases 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() }; } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index a0b35dc132747..15566344a8e86 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -72,6 +72,7 @@ + @@ -141,4 +142,4 @@ - \ No newline at end of file +