Skip to content

Commit

Permalink
Adds de/serialization support for JsonDocument (#34537)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
marcusturewicz authored Apr 6, 2020
1 parent d02ee5b commit 5dcf3de
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
<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" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public sealed partial class JsonSerializerOptions

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.
Expand All @@ -55,6 +55,7 @@ private static Dictionary<Type, JsonConverter> GetDefaultSimpleConverters()
Add(new Int32Converter());
Add(new Int64Converter());
Add(new JsonElementConverter());
Add(new JsonDocumentConverter());
Add(new ObjectConverter());
Add(new SByteConverter());
Add(new SingleConverter());
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ public static IEnumerable<object[]> 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() };
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down Expand Up @@ -141,4 +142,4 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
</ItemGroup>
</Project>
</Project>

0 comments on commit 5dcf3de

Please sign in to comment.