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