Skip to content

Commit

Permalink
Add support to JSON codecs to serialize their generic JSON types (#9033)
Browse files Browse the repository at this point in the history
  • Loading branch information
ReubenBond committed Jun 2, 2024
1 parent f6659d9 commit 6f1ffee
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 29 deletions.
23 changes: 23 additions & 0 deletions src/Orleans.Serialization.NewtonsoftJson/NewtonsoftJsonCodec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Reflection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Orleans.Metadata;
using Orleans.Serialization.Buffers;
using Orleans.Serialization.Buffers.Adaptors;
Expand Down Expand Up @@ -132,6 +133,11 @@ bool IGeneralizedCodec.IsSupportedType(Type type)
return false;
}

if (IsNativelySupportedType(type))
{
return true;
}

foreach (var selector in _serializableTypeSelectors)
{
if (selector.IsSupportedType(type))
Expand Down Expand Up @@ -186,6 +192,11 @@ bool IGeneralizedCopier.IsSupportedType(Type type)
return false;
}

if (IsNativelySupportedType(type))
{
return true;
}

foreach (var selector in _copyableTypeSelectors)
{
if (selector.IsSupportedType(type))
Expand All @@ -202,6 +213,18 @@ bool IGeneralizedCopier.IsSupportedType(Type type)
return false;
}

private static bool IsNativelySupportedType(Type type)
{
return type == typeof(JObject)
|| type == typeof(JArray)
|| type == typeof(JProperty)
|| type == typeof(JRaw)
|| type == typeof(JValue)
|| type == typeof(JConstructor)
|| typeof(JContainer).IsAssignableFrom(type)
|| typeof(JToken).IsAssignableFrom(type);
}

private static void ThrowTypeFieldMissing() => throw new RequiredFieldMissingException("Serialized value is missing its type field.");

private bool IsSupportedType(Type type) => ((IGeneralizedCodec)this).IsSupportedType(type) || ((IGeneralizedCopier)this).IsSupportedType(type);
Expand Down
23 changes: 23 additions & 0 deletions src/Orleans.Serialization.SystemTextJson/JsonCodec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Options;
using Orleans.Metadata;
using Orleans.Serialization.Buffers;
Expand Down Expand Up @@ -165,6 +166,11 @@ bool IGeneralizedCodec.IsSupportedType(Type type)
return false;
}

if (IsNativelySupportedType(type))
{
return true;
}

foreach (var selector in _serializableTypeSelectors)
{
if (selector.IsSupportedType(type))
Expand All @@ -181,6 +187,18 @@ bool IGeneralizedCodec.IsSupportedType(Type type)
return false;
}

private static bool IsNativelySupportedType(Type type)
{
// Add types natively supported by System.Text.Json
// From https://github.com/dotnet/runtime/blob/2c4d0df3b146f8322f676b83a53ca21a065bdfc7/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs#L1792-L1797
return type == typeof(JsonArray)
|| type == typeof(JsonElement)
|| type == typeof(JsonObject)
|| type == typeof(JsonDocument)
|| typeof(JsonNode).IsAssignableFrom(type)
|| typeof(JsonValue).IsAssignableFrom(type);
}

/// <inheritdoc/>
object IDeepCopier.DeepCopy(object input, CopyContext context)
{
Expand Down Expand Up @@ -216,6 +234,11 @@ bool IGeneralizedCopier.IsSupportedType(Type type)
return false;
}

if (IsNativelySupportedType(type))
{
return true;
}

foreach (var selector in _copyableTypeSelectors)
{
if (selector.IsSupportedType(type))
Expand Down
36 changes: 30 additions & 6 deletions test/Orleans.Serialization.UnitTests/JsonSerializerTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Reflection;
using System.Text.Json.Nodes;
using Microsoft.Extensions.DependencyInjection;
using Orleans.Serialization.Cloning;
using Orleans.Serialization.Codecs;
Expand Down Expand Up @@ -75,21 +76,30 @@ public void JsonSerializerRoundTripThroughUntypedSerializer()
Assert.Equal(original.IntProperty, result.IntProperty);
Assert.Equal(original.SubTypeProperty, result.SubTypeProperty);
}
}

[Trait("Category", "BVT")]
public class JsonCodecCopierTests : CopierTester<MyJsonClass?, IDeepCopier<MyJsonClass?>>
{
public JsonCodecCopierTests(ITestOutputHelper output) : base(output)
[Fact]
public void CanSerializeNativeJsonTypes()
{
JsonArray jsonArray = new JsonArray([JsonValue.Create(true), JsonValue.Create(42), JsonValue.Create("hello")]);
JsonObject? jsonObject = System.Text.Json.JsonSerializer.Deserialize<JsonObject>("{\"foo\": \"bar\"}");

var deserializedArray = RoundTripThroughUntypedSerializer(jsonArray, out _);
Assert.Equal(System.Text.Json.JsonSerializer.Serialize(jsonArray), System.Text.Json.JsonSerializer.Serialize(deserializedArray));

var deserializedObject = RoundTripThroughUntypedSerializer(jsonObject, out _);
Assert.Equal(System.Text.Json.JsonSerializer.Serialize(jsonObject), System.Text.Json.JsonSerializer.Serialize(deserializedObject));
}
}

[Trait("Category", "BVT")]
public class JsonCodecCopierTests(ITestOutputHelper output) : CopierTester<MyJsonClass?, IDeepCopier<MyJsonClass?>>(output)
{
protected override void Configure(ISerializerBuilder builder)
{
builder.AddJsonSerializer(isSupported: type => type.GetCustomAttribute<MyJsonSerializableAttribute>(inherit: false) is not null);
}
protected override IDeepCopier<MyJsonClass?> CreateCopier() => ServiceProvider.GetRequiredService<ICodecProvider>().GetDeepCopier<MyJsonClass?>();

protected override IDeepCopier<MyJsonClass?> CreateCopier() => ServiceProvider.GetRequiredService<ICodecProvider>().GetDeepCopier<MyJsonClass?>();

protected override MyJsonClass? CreateValue() => new MyJsonClass { IntProperty = 30, SubTypeProperty = "hello", Id = new(Guid.NewGuid()) };

Expand All @@ -100,5 +110,19 @@ protected override void Configure(ISerializerBuilder builder)
new MyJsonClass() { IntProperty = 150, SubTypeProperty = new string('c', 20), Id = new(Guid.NewGuid()) },
new MyJsonClass() { IntProperty = -150_000, SubTypeProperty = new string('c', 6_000), Id = new(Guid.NewGuid()) },
};

[Fact]
public void CanCopyNativeJsonTypes()
{
JsonArray jsonArray = new JsonArray([JsonValue.Create(true), JsonValue.Create(42), JsonValue.Create("hello")]);
JsonObject? jsonObject = System.Text.Json.JsonSerializer.Deserialize<JsonObject>("{\"foo\": \"bar\"}");
var copier = ServiceProvider.GetRequiredService<DeepCopier>();

var deserializedArray = copier.Copy(jsonArray);
Assert.Equal(System.Text.Json.JsonSerializer.Serialize(jsonArray), System.Text.Json.JsonSerializer.Serialize(deserializedArray));

var deserializedObject = copier.Copy(jsonObject);
Assert.Equal(System.Text.Json.JsonSerializer.Serialize(jsonObject), System.Text.Json.JsonSerializer.Serialize(deserializedObject));
}
}
}
41 changes: 40 additions & 1 deletion test/Orleans.Serialization.UnitTests/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
using System.Linq.Expressions;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Orleans;

[Alias("test.person.alias"), GenerateSerializer]
Expand Down Expand Up @@ -64,6 +67,11 @@ public sealed class MyJsonSerializableAttribute : Attribute
{
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class MyNewtonsoftJsonSerializableAttribute : Attribute
{
}

internal interface IMyBase
{
MyValue BaseValue { get; set; }
Expand Down Expand Up @@ -389,6 +397,29 @@ public class MyNonJsonBaseClass : IEquatable<MyNonJsonBaseClass>
public override int GetHashCode() => HashCode.Combine(IntProperty);
}

[MyNewtonsoftJsonSerializable]
public class MyNewtonsoftJsonClass : MyNonJsonBaseClass, IEquatable<MyNewtonsoftJsonClass>
{
[JsonProperty]
public string SubTypeProperty { get; set; }

[JsonProperty]
public TestId Id { get; set; }

[JsonProperty]
public JArray JsonArray { get; set; } = new JArray(true, 42, "hello");

[JsonProperty]
public JObject JsonObject { get; set; } = new() { ["foo"] = "bar" };

public override string ToString() => $"{nameof(SubTypeProperty)}: {SubTypeProperty}, {base.ToString()}";
public bool Equals(MyNewtonsoftJsonClass other) => other is not null && base.Equals(other) && string.Equals(SubTypeProperty, other.SubTypeProperty, StringComparison.Ordinal) && EqualityComparer<TestId>.Default.Equals(Id, other.Id)
&& string.Equals(JsonConvert.SerializeObject(JsonArray), JsonConvert.SerializeObject(other.JsonArray))
&& string.Equals(JsonConvert.SerializeObject(JsonObject), JsonConvert.SerializeObject(other.JsonObject));
public override bool Equals(object obj) => Equals(obj as MyJsonClass);
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SubTypeProperty);
}

[MyJsonSerializable]
public class MyJsonClass : MyNonJsonBaseClass, IEquatable<MyJsonClass>
{
Expand All @@ -398,8 +429,16 @@ public class MyJsonClass : MyNonJsonBaseClass, IEquatable<MyJsonClass>
[JsonProperty]
public TestId Id { get; set; }

[JsonProperty]
public JsonArray JsonArray { get; set; } = new JsonArray(true, 42, "hello");

[JsonProperty]
public JsonObject JsonObject { get; set; } = new() { ["foo"] = "bar" };

public override string ToString() => $"{nameof(SubTypeProperty)}: {SubTypeProperty}, {base.ToString()}";
public bool Equals(MyJsonClass other) => other is not null && base.Equals(other) && string.Equals(SubTypeProperty, other.SubTypeProperty, StringComparison.Ordinal) && EqualityComparer<TestId>.Default.Equals(Id, other.Id);
public bool Equals(MyJsonClass other) => other is not null && base.Equals(other) && string.Equals(SubTypeProperty, other.SubTypeProperty, StringComparison.Ordinal) && EqualityComparer<TestId>.Default.Equals(Id, other.Id)
&& string.Equals(System.Text.Json.JsonSerializer.Serialize(JsonArray), System.Text.Json.JsonSerializer.Serialize(other.JsonArray))
&& string.Equals(System.Text.Json.JsonSerializer.Serialize(JsonObject), System.Text.Json.JsonSerializer.Serialize(other.JsonObject));
public override bool Equals(object obj) => Equals(obj as MyJsonClass);
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SubTypeProperty);
}
Expand Down
73 changes: 51 additions & 22 deletions test/Orleans.Serialization.UnitTests/NewtonsoftJsonCodecTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using Orleans.Serialization.Cloning;
using Orleans.Serialization.Codecs;
using Orleans.Serialization.Serializers;
Expand All @@ -10,34 +11,34 @@
namespace Orleans.Serialization.UnitTests
{
[Trait("Category", "BVT")]
public class NewtonsoftJsonCodecTests : FieldCodecTester<MyJsonClass, IFieldCodec<MyJsonClass>>
public class NewtonsoftJsonCodecTests : FieldCodecTester<MyNewtonsoftJsonClass, IFieldCodec<MyNewtonsoftJsonClass>>
{
public NewtonsoftJsonCodecTests(ITestOutputHelper output) : base(output)
{
}

protected override void Configure(ISerializerBuilder builder)
{
builder.AddNewtonsoftJsonSerializer(isSupported: type => type.GetCustomAttribute<MyJsonSerializableAttribute>(inherit: false) is not null);
builder.AddNewtonsoftJsonSerializer(isSupported: type => type.GetCustomAttribute<MyNewtonsoftJsonSerializableAttribute>(inherit: false) is not null);
}

protected override MyJsonClass CreateValue() => new MyJsonClass { IntProperty = 30, SubTypeProperty = "hello" };
protected override MyNewtonsoftJsonClass CreateValue() => new MyNewtonsoftJsonClass { IntProperty = 30, SubTypeProperty = "hello" };

protected override int[] MaxSegmentSizes => new[] { 840 };

protected override MyJsonClass[] TestValues => new MyJsonClass[]
protected override MyNewtonsoftJsonClass[] TestValues => new MyNewtonsoftJsonClass[]
{
null,
new MyJsonClass(),
new MyJsonClass() { IntProperty = 150, SubTypeProperty = new string('c', 20) },
new MyJsonClass() { IntProperty = -150_000, SubTypeProperty = new string('c', 4097) },
new MyNewtonsoftJsonClass(),
new MyNewtonsoftJsonClass() { IntProperty = 150, SubTypeProperty = new string('c', 20) },
new MyNewtonsoftJsonClass() { IntProperty = -150_000, SubTypeProperty = new string('c', 4097) },
};

[Fact]
public void NewtonsoftJsonDeepCopyTyped()
{
var original = new MyJsonClass { IntProperty = 30, SubTypeProperty = "hi" };
var copier = ServiceProvider.GetRequiredService<DeepCopier<MyJsonClass>>();
var original = new MyNewtonsoftJsonClass { IntProperty = 30, SubTypeProperty = "hi" };
var copier = ServiceProvider.GetRequiredService<DeepCopier<MyNewtonsoftJsonClass>>();
var result = copier.Copy(original);

Assert.Equal(original.IntProperty, result.IntProperty);
Expand All @@ -47,9 +48,9 @@ public void NewtonsoftJsonDeepCopyTyped()
[Fact]
public void NewtonsoftJsonDeepCopyUntyped()
{
var original = new MyJsonClass { IntProperty = 30, SubTypeProperty = "hi" };
var original = new MyNewtonsoftJsonClass { IntProperty = 30, SubTypeProperty = "hi" };
var copier = ServiceProvider.GetRequiredService<DeepCopier>();
var result = (MyJsonClass)copier.Copy((object)original);
var result = (MyNewtonsoftJsonClass)copier.Copy((object)original);

Assert.Equal(original.IntProperty, result.IntProperty);
Assert.Equal(original.SubTypeProperty, result.SubTypeProperty);
Expand All @@ -58,7 +59,7 @@ public void NewtonsoftJsonDeepCopyUntyped()
[Fact]
public void NewtonsoftJsonRoundTripThroughCodec()
{
var original = new MyJsonClass { IntProperty = 30, SubTypeProperty = "hi" };
var original = new MyNewtonsoftJsonClass { IntProperty = 30, SubTypeProperty = "hi" };
var result = RoundTripThroughCodec(original);

Assert.Equal(original.IntProperty, result.IntProperty);
Expand All @@ -68,37 +69,65 @@ public void NewtonsoftJsonRoundTripThroughCodec()
[Fact]
public void NewtonsoftJsonRoundTripThroughUntypedSerializer()
{
var original = new MyJsonClass { IntProperty = 30, SubTypeProperty = "hi" };
var original = new MyNewtonsoftJsonClass { IntProperty = 30, SubTypeProperty = "hi" };
var untypedResult = RoundTripThroughUntypedSerializer(original, out _);

var result = Assert.IsType<MyJsonClass>(untypedResult);
var result = Assert.IsType<MyNewtonsoftJsonClass>(untypedResult);
Assert.Equal(original.IntProperty, result.IntProperty);
Assert.Equal(original.SubTypeProperty, result.SubTypeProperty);
}

[Fact]
public void CanSerializeNativeJsonTypes()
{
var jsonArray = Newtonsoft.Json.JsonConvert.DeserializeObject<JArray>("[1, true, \"three\", {\"foo\": \"bar\"}]");
var jsonObject = Newtonsoft.Json.JsonConvert.DeserializeObject<JObject>("{\"foo\": \"bar\"}");
var copier = ServiceProvider.GetRequiredService<DeepCopier>();

var deserializedArray = RoundTripThroughUntypedSerializer(jsonArray, out _);
Assert.Equal(Newtonsoft.Json.JsonConvert.SerializeObject(jsonArray), Newtonsoft.Json.JsonConvert.SerializeObject(deserializedArray));

var deserializedObject = RoundTripThroughUntypedSerializer(jsonObject, out _);
Assert.Equal(Newtonsoft.Json.JsonConvert.SerializeObject(jsonObject), Newtonsoft.Json.JsonConvert.SerializeObject(deserializedObject));
}
}

[Trait("Category", "BVT")]
public class NewtonsoftJsonCodecCopierTests : CopierTester<MyJsonClass, IDeepCopier<MyJsonClass>>
public class NewtonsoftJsonCodecCopierTests : CopierTester<MyNewtonsoftJsonClass, IDeepCopier<MyNewtonsoftJsonClass>>
{
public NewtonsoftJsonCodecCopierTests(ITestOutputHelper output) : base(output)
{
}

protected override void Configure(ISerializerBuilder builder)
{
builder.AddNewtonsoftJsonSerializer(isSupported: type => type.GetCustomAttribute<MyJsonSerializableAttribute>(inherit: false) is not null);
builder.AddNewtonsoftJsonSerializer(isSupported: type => type.GetCustomAttribute<MyNewtonsoftJsonSerializableAttribute>(inherit: false) is not null);
}

protected override IDeepCopier<MyJsonClass> CreateCopier() => ServiceProvider.GetRequiredService<ICodecProvider>().GetDeepCopier<MyJsonClass>();
protected override IDeepCopier<MyNewtonsoftJsonClass> CreateCopier() => ServiceProvider.GetRequiredService<ICodecProvider>().GetDeepCopier<MyNewtonsoftJsonClass>();

protected override MyJsonClass CreateValue() => new MyJsonClass { IntProperty = 30, SubTypeProperty = "hello" };
protected override MyNewtonsoftJsonClass CreateValue() => new MyNewtonsoftJsonClass { IntProperty = 30, SubTypeProperty = "hello" };

protected override MyJsonClass[] TestValues => new MyJsonClass[]
protected override MyNewtonsoftJsonClass[] TestValues => new MyNewtonsoftJsonClass[]
{
null,
new MyJsonClass(),
new MyJsonClass() { IntProperty = 150, SubTypeProperty = new string('c', 20) },
new MyJsonClass() { IntProperty = -150_000, SubTypeProperty = new string('c', 4097) },
new MyNewtonsoftJsonClass(),
new MyNewtonsoftJsonClass() { IntProperty = 150, SubTypeProperty = new string('c', 20) },
new MyNewtonsoftJsonClass() { IntProperty = -150_000, SubTypeProperty = new string('c', 4097) },
};

[Fact]
public void CanCopyNativeJsonTypes()
{
var jsonArray = Newtonsoft.Json.JsonConvert.DeserializeObject<JArray>("[1, true, \"three\", {\"foo\": \"bar\"}]");
var jsonObject = Newtonsoft.Json.JsonConvert.DeserializeObject<JObject>("{\"foo\": \"bar\"}");
var copier = ServiceProvider.GetRequiredService<DeepCopier>();

var deserializedArray = copier.Copy(jsonArray);
Assert.Equal(Newtonsoft.Json.JsonConvert.SerializeObject(jsonArray), Newtonsoft.Json.JsonConvert.SerializeObject(deserializedArray));

var deserializedObject = copier.Copy(jsonObject);
Assert.Equal(Newtonsoft.Json.JsonConvert.SerializeObject(jsonObject), Newtonsoft.Json.JsonConvert.SerializeObject(deserializedObject));
}
}
}

0 comments on commit 6f1ffee

Please sign in to comment.