Repository: dotnet/orleans (2025-07-11T23:42:21Z)

Issue:
System.InvalidCastException: Unable to cast object of type 'System.Text.Json.Nodes.JsonValueOfElement' to type 'System.Text.Json.Nodes.JsonValuePrimitive`1[System.Int32]'.
When a Grain has JsonNode parameter, calling it with a JsonValue creates the following exception:
System.InvalidCastException: Unable to cast object of type 'System.Text.Json.Nodes.JsonValueOfElement' to type 'System.Text.Json.Nodes.JsonValuePrimitive`1[System.Int32]'.


Grain Example:
```c#

public interface ITestGrain : IGrainWithStringKey
{
      Task InvokeAsync(JsonNode? input);
}

// call example
var grain = ClusterClient.GetGrain<ITestGrain>(string.Empty);
grain.InvokeAsync(JsonValue.Create(1)); // <-- This throws the exception, using a JsonObject here does not throw an exception
```


Discussion:

Patch:
diff --git a/src/Orleans.Serialization.SystemTextJson/JsonCodec.cs b/src/Orleans.Serialization.SystemTextJson/JsonCodec.cs
index ab8ace8b292..e40eb38d8a6 100644
--- a/src/Orleans.Serialization.SystemTextJson/JsonCodec.cs
+++ b/src/Orleans.Serialization.SystemTextJson/JsonCodec.cs
@@ -73,7 +73,7 @@ void IFieldCodec.WriteField<TBufferWriter>(ref Writer<TBufferWriter> writer, uin
         var bufferWriter = new BufferWriterBox<PooledBuffer>(new PooledBuffer());
         try
         {
-            var jsonWriter = new Utf8JsonWriter(bufferWriter);
+            var jsonWriter = new Utf8JsonWriter(bufferWriter, _options.WriterOptions);
             JsonSerializer.Serialize(jsonWriter, value, _options.SerializerOptions);
             jsonWriter.Flush();
 
@@ -135,7 +135,14 @@ object IFieldCodec.ReadValue<TInput>(ref Reader<TInput> reader, Field field)
                         reader.ReadBytes(ref tempBuffer, (int)length);
                         var sequence = tempBuffer.AsReadOnlySequence();
                         var jsonReader = new Utf8JsonReader(sequence, _options.ReaderOptions);
-                        result = JsonSerializer.Deserialize(ref jsonReader, type, _options.SerializerOptions);
+                        if (typeof(JsonNode).IsAssignableFrom(type))
+                        {
+                            result = JsonNode.Parse(ref jsonReader);
+                        }
+                        else
+                        {
+                            result = JsonSerializer.Deserialize(ref jsonReader, type, _options.SerializerOptions);
+                        }
                     }
                     finally
                     {
@@ -191,25 +198,33 @@ 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)
+        return type == typeof(JsonElement)
             || type == typeof(JsonDocument)
-            || typeof(JsonNode).IsAssignableFrom(type)
-            || typeof(JsonValue).IsAssignableFrom(type);
+            || type == typeof(JsonArray)
+            || type == typeof(JsonObject)
+            || type == typeof(JsonValue)
+            || typeof(JsonNode).IsAssignableFrom(type);
     }
 
     /// <inheritdoc/>
     object IDeepCopier.DeepCopy(object input, CopyContext context)
     {
         if (context.TryGetCopy(input, out object result))
+        {
             return result;
+        }
 
+        if (input is JsonNode jsonNode)
+        {
+            var copy = jsonNode.DeepClone();
+            context.RecordCopy(input, copy);
+            return copy;
+        }
 
         var bufferWriter = new BufferWriterBox<PooledBuffer>(new PooledBuffer());
         try
         {
-            var jsonWriter = new Utf8JsonWriter(bufferWriter);
+            var jsonWriter = new Utf8JsonWriter(bufferWriter, _options.WriterOptions);
             JsonSerializer.Serialize(jsonWriter, input, _options.SerializerOptions);
             jsonWriter.Flush();
 
@@ -259,4 +274,4 @@ bool IGeneralizedCopier.IsSupportedType(Type type)
     bool? ITypeFilter.IsTypeAllowed(Type type) => (((IGeneralizedCopier)this).IsSupportedType(type) || ((IGeneralizedCodec)this).IsSupportedType(type)) ? true : null;
 
     private static void ThrowTypeFieldMissing() => throw new RequiredFieldMissingException("Serialized value is missing its type field.");
-}
\ No newline at end of file
+}
diff --git a/src/Orleans.Serialization.SystemTextJson/JsonCodecOptions.cs b/src/Orleans.Serialization.SystemTextJson/JsonCodecOptions.cs
index 39cc838a267..496559bd52e 100644
--- a/src/Orleans.Serialization.SystemTextJson/JsonCodecOptions.cs
+++ b/src/Orleans.Serialization.SystemTextJson/JsonCodecOptions.cs
@@ -18,6 +18,11 @@ public class JsonCodecOptions
     /// </summary>
     public JsonReaderOptions ReaderOptions { get; set; }
 
+    /// <summary>
+    /// Gets or sets the <see cref="JsonWriterOptions"/>.
+    /// </summary>
+    public JsonWriterOptions WriterOptions { get; set; }
+
     /// <summary>
     /// Gets or sets a delegate used to determine if a type is supported by the JSON serializer for serialization and deserialization.
     /// </summary>
@@ -27,4 +32,4 @@ public class JsonCodecOptions
     /// Gets or sets a delegate used to determine if a type is supported by the JSON serializer for copying.
     /// </summary>
     public Func<Type, bool?> IsCopyableType { get; set; }
-}
\ No newline at end of file
+}


Test patch:
diff --git a/test/Orleans.Serialization.UnitTests/Buffers/Adaptors/PooledBufferStreamTests.cs b/test/Orleans.Serialization.UnitTests/Buffers/Adaptors/PooledBufferStreamTests.cs
index 459e092b4d8..d4da1dee53c 100644
--- a/test/Orleans.Serialization.UnitTests/Buffers/Adaptors/PooledBufferStreamTests.cs
+++ b/test/Orleans.Serialization.UnitTests/Buffers/Adaptors/PooledBufferStreamTests.cs
@@ -765,7 +765,7 @@ public static IEnumerable<object[]> InvalidWriteParameters
     [MemberData(nameof(InvalidWriteParameters))]
     public void Write_InvalidParameters_ThrowsException(byte[] buffer, int offset, int count, Type expectedException)
     {
-        using var stream = new MemoryStream();
+        using var stream = new PooledBufferStream();
         Assert.Throws(expectedException, () => stream.Write(buffer, offset, count));
     }
 
diff --git a/test/Orleans.Serialization.UnitTests/JsonSerializerTests.cs b/test/Orleans.Serialization.UnitTests/JsonSerializerTests.cs
index f65727c08c0..43fd87b0ef7 100644
--- a/test/Orleans.Serialization.UnitTests/JsonSerializerTests.cs
+++ b/test/Orleans.Serialization.UnitTests/JsonSerializerTests.cs
@@ -1,6 +1,8 @@
 #nullable enable
 using System;
+using System.Linq;
 using System.Reflection;
+using System.Text.Json;
 using System.Text.Json.Nodes;
 using Microsoft.Extensions.DependencyInjection;
 using Orleans.Serialization.Cloning;
@@ -14,12 +16,12 @@ namespace Orleans.Serialization.UnitTests
 {
     /// <summary>
     /// Tests for Orleans' JSON serialization support using System.Text.Json.
-    /// 
+    ///
     /// Orleans provides integration with System.Text.Json for scenarios where:
     /// - JSON format is required for interoperability
     /// - Human-readable serialization is needed for debugging
     /// - Integration with external systems that expect JSON
-    /// 
+    ///
     /// The JSON codec in Orleans:
     /// - Can be selectively applied to specific types using predicates
     /// - Supports polymorphic serialization through Orleans' type system
@@ -48,63 +50,165 @@ protected override void Configure(ISerializerBuilder builder)
             new MyJsonClass() { IntProperty = -150_000, SubTypeProperty = new string('c', 6_000), Id = new(Guid.NewGuid()) },
         };
 
+        /// <summary>
+        /// Tests that custom types can be deep copied through the JSON codec.
+        /// Verifies Orleans' deep copy functionality with JSON serialization.
+        /// </summary>
         [Fact]
-        public void JsonSerializerDeepCopyTyped()
+        public void JsonSerializerDeepCopy()
         {
             var original = new MyJsonClass { IntProperty = 30, SubTypeProperty = "hi", Id = new(Guid.NewGuid()) };
-            var copier = ServiceProvider.GetRequiredService<DeepCopier<MyJsonClass>>();
-            var result = copier.Copy(original);
 
-            Assert.Equal(original.IntProperty, result.IntProperty);
-            Assert.Equal(original.SubTypeProperty, result.SubTypeProperty);
+            // Test typed copier
+            var typedCopier = ServiceProvider.GetRequiredService<DeepCopier<MyJsonClass>>();
+            var typedResult = typedCopier.Copy(original);
+            Assert.Equal(original.IntProperty, typedResult.IntProperty);
+            Assert.Equal(original.SubTypeProperty, typedResult.SubTypeProperty);
+
+            // Test untyped copier
+            var untypedCopier = ServiceProvider.GetRequiredService<DeepCopier>();
+            var untypedResult = (MyJsonClass)untypedCopier.Copy((object)original);
+            Assert.Equal(original.IntProperty, untypedResult.IntProperty);
+            Assert.Equal(original.SubTypeProperty, untypedResult.SubTypeProperty);
         }
 
+        /// <summary>
+        /// Tests round-trip serialization through Orleans' codec and serializer systems.
+        /// Verifies both typed and untyped serialization paths work correctly.
+        /// </summary>
         [Fact]
-        public void JsonSerializerDeepCopyUntyped()
+        public void JsonSerializerRoundTrip()
         {
             var original = new MyJsonClass { IntProperty = 30, SubTypeProperty = "hi", Id = new(Guid.NewGuid()) };
-            var copier = ServiceProvider.GetRequiredService<DeepCopier>();
-            var result = (MyJsonClass)copier.Copy((object)original);
 
+            // Test through codec
+            var codecResult = RoundTripThroughCodec(original);
+            Assert.Equal(original.IntProperty, codecResult.IntProperty);
+            Assert.Equal(original.SubTypeProperty, codecResult.SubTypeProperty);
+
+            // Test through untyped serializer
+            var untypedResult = RoundTripThroughUntypedSerializer(original, out _);
+            var result = Assert.IsType<MyJsonClass>(untypedResult);
             Assert.Equal(original.IntProperty, result.IntProperty);
             Assert.Equal(original.SubTypeProperty, result.SubTypeProperty);
         }
 
+        /// <summary>
+        /// Verifies JsonValue can be serialized when stored as JsonNode.
+        /// </summary>
         [Fact]
-        public void JsonSerializerRoundTripThroughCodec()
+        public void CanSerializeJsonValue_AsJsonNode()
         {
-            var original = new MyJsonClass { IntProperty = 30, SubTypeProperty = "hi", Id = new(Guid.NewGuid()) };
-            var result = RoundTripThroughCodec(original);
+            // Test untyped serialization (the core issue scenario)
+            var jsonValueInt = JsonValue.Create(1);
+            var jsonValueString = JsonValue.Create("hello");
+            var jsonValueBool = JsonValue.Create(true);
 
-            Assert.Equal(original.IntProperty, result.IntProperty);
-            Assert.Equal(original.SubTypeProperty, result.SubTypeProperty);
+            var deserializedInt = RoundTripThroughUntypedSerializer(jsonValueInt, out _);
+            Assert.Equal(JsonSerializer.Serialize(jsonValueInt), JsonSerializer.Serialize(deserializedInt));
+
+            var deserializedString = RoundTripThroughUntypedSerializer(jsonValueString, out _);
+            Assert.Equal(JsonSerializer.Serialize(jsonValueString), JsonSerializer.Serialize(deserializedString));
+
+            var deserializedBool = RoundTripThroughUntypedSerializer(jsonValueBool, out _);
+            Assert.Equal(JsonSerializer.Serialize(jsonValueBool), JsonSerializer.Serialize(deserializedBool));
+
+            // Test typed serialization (for completeness)
+            var serializer = ServiceProvider.GetRequiredService<Serializer>();
+            JsonNode jsonNode = JsonValue.Create(1);
+
+            var serialized = serializer.SerializeToArray(jsonNode);
+            var deserialized = serializer.Deserialize<JsonNode>(serialized);
+            Assert.Equal(1, deserialized.GetValue<int>());
         }
 
+        /// <summary>
+        /// Tests serialization of all JsonNode subtypes through Orleans.
+        /// Verifies polymorphic serialization works correctly.
+        /// </summary>
         [Fact]
-        public void JsonSerializerRoundTripThroughUntypedSerializer()
+        public void CanSerializeAllJsonNodeTypes()
         {
-            var original = new MyJsonClass { IntProperty = 30, SubTypeProperty = "hi", Id = new(Guid.NewGuid()) };
-            var untypedResult = RoundTripThroughUntypedSerializer(original, out _);
+            var serializer = ServiceProvider.GetRequiredService<Serializer>();
 
-            var result = Assert.IsType<MyJsonClass>(untypedResult);
-            Assert.Equal(original.IntProperty, result.IntProperty);
-            Assert.Equal(original.SubTypeProperty, result.SubTypeProperty);
+            // Test JsonValue
+            JsonNode valueNode = JsonValue.Create(42);
+            var valueDeserialized = serializer.Deserialize<JsonNode>(serializer.SerializeToArray(valueNode));
+            Assert.Equal(42, valueDeserialized.GetValue<int>());
+
+            // Test JsonObject
+            JsonNode objectNode = new JsonObject { ["key"] = "value", ["number"] = 123 };
+            var objectDeserialized = serializer.Deserialize<JsonNode>(serializer.SerializeToArray(objectNode));
+            Assert.IsType<JsonObject>(objectDeserialized);
+            Assert.Equal("value", objectDeserialized["key"]!.GetValue<string>());
+
+            // Test JsonArray
+            JsonNode arrayNode = new JsonArray { 1, 2, 3 };
+            var arrayDeserialized = serializer.Deserialize<JsonNode>(serializer.SerializeToArray(arrayNode));
+            Assert.IsType<JsonArray>(arrayDeserialized);
+            Assert.Equal(3, ((JsonArray)arrayDeserialized).Count);
+
+            // Test complex nested structure
+            var complexNode = new JsonObject
+            {
+                ["array"] = new JsonArray { 1, 2, 3 },
+                ["object"] = new JsonObject { ["nested"] = true },
+                ["value"] = JsonValue.Create("test"),
+                ["null"] = null
+            };
+
+            var complexDeserialized = serializer.Deserialize<JsonNode>(serializer.SerializeToArray(complexNode));
+            Assert.NotNull(complexDeserialized);
+            Assert.IsType<JsonArray>(complexDeserialized["array"]);
+            Assert.IsType<JsonObject>(complexDeserialized["object"]);
+            Assert.IsAssignableFrom<JsonValue>(complexDeserialized["value"]);
+            Assert.Null(complexDeserialized["null"]);
         }
 
+        /// <summary>
+        /// Tests Orleans serialization of JsonDocument and JsonElement types.
+        /// Verifies these types work correctly with Orleans' serialization system.
+        /// </summary>
         [Fact]
-        public void CanSerializeNativeJsonTypes()
+        public void CanSerializeJsonDocumentAndElement()
         {
-            JsonArray jsonArray = new JsonArray([JsonValue.Create(true), JsonValue.Create(42), JsonValue.Create("hello")]);
-            JsonObject? jsonObject = System.Text.Json.JsonSerializer.Deserialize<JsonObject>("{\"foo\": \"bar\"}");
+            var serializer = ServiceProvider.GetRequiredService<Serializer>();
+
+            var jsonString = @"{
+                ""data"": [1, 2, 3],
+                ""metadata"": {
+                    ""version"": 1,
+                    ""active"": true
+                }
+            }";
+
+            using var doc = JsonDocument.Parse(jsonString);
 
-            var deserializedArray = RoundTripThroughUntypedSerializer(jsonArray, out _);
-            Assert.Equal(System.Text.Json.JsonSerializer.Serialize(jsonArray), System.Text.Json.JsonSerializer.Serialize(deserializedArray));
+            // Test JsonElement serialization
+            var element = doc.RootElement;
+            var serializedElement = serializer.SerializeToArray(element);
+            var deserializedElement = serializer.Deserialize<JsonElement>(serializedElement);
 
-            var deserializedObject = RoundTripThroughUntypedSerializer(jsonObject, out _);
-            Assert.Equal(System.Text.Json.JsonSerializer.Serialize(jsonObject), System.Text.Json.JsonSerializer.Serialize(deserializedObject));
+            Assert.Equal(JsonValueKind.Object, deserializedElement.ValueKind);
+            Assert.Equal(3, deserializedElement.GetProperty("data").GetArrayLength());
+            Assert.True(deserializedElement.GetProperty("metadata").GetProperty("active").GetBoolean());
+
+            // Test JsonDocument serialization
+            var serializedDoc = serializer.SerializeToArray(doc);
+            var deserializedDoc = serializer.Deserialize<JsonDocument>(serializedDoc);
+
+            Assert.NotNull(deserializedDoc);
+            Assert.Equal(doc.RootElement.GetProperty("data").GetArrayLength(),
+                        deserializedDoc.RootElement.GetProperty("data").GetArrayLength());
+
+            deserializedDoc.Dispose();
         }
     }
 
+    /// <summary>
+    /// Tests for the JsonCodec deep copy functionality.
+    /// Focuses on Orleans' deep copy integration with JSON types.
+    /// </summary>
     [Trait("Category", "BVT")]
     public class JsonCodecCopierTests(ITestOutputHelper output) : CopierTester<MyJsonClass?, IDeepCopier<MyJsonClass?>>(output)
     {
@@ -125,18 +229,53 @@ protected override void Configure(ISerializerBuilder builder)
             new MyJsonClass() { IntProperty = -150_000, SubTypeProperty = new string('c', 6_000), Id = new(Guid.NewGuid()) },
         };
 
+        /// <summary>
+        /// Tests deep copying of native JSON types through Orleans.
+        /// Verifies the copier works with JsonNode subtypes.
+        /// </summary>
         [Fact]
         public void CanCopyNativeJsonTypes()
         {
+            var copier = ServiceProvider.GetRequiredService<DeepCopier>();
+
+            var jsonValue = JsonValue.Create(42);
+            var copiedValue = copier.Copy(jsonValue);
+            Assert.Equal(JsonSerializer.Serialize(jsonValue), JsonSerializer.Serialize(copiedValue));
+
+            // Test JsonArray copying
             JsonArray jsonArray = new JsonArray([JsonValue.Create(true), JsonValue.Create(42), JsonValue.Create("hello")]);
-            JsonObject? jsonObject = System.Text.Json.JsonSerializer.Deserialize<JsonObject>("{\"foo\": \"bar\"}");
+            var copiedArray = copier.Copy(jsonArray);
+            Assert.Equal(JsonSerializer.Serialize(jsonArray), JsonSerializer.Serialize(copiedArray));
+
+            // Test JsonObject copying
+            JsonObject? jsonObject = JsonSerializer.Deserialize<JsonObject>("{\"foo\": \"bar\"}");
+            var copiedObject = copier.Copy(jsonObject);
+            Assert.Equal(JsonSerializer.Serialize(jsonObject), JsonSerializer.Serialize(copiedObject));
+        }
+
+        /// <summary>
+        /// Tests deep copying of JsonDocument and JsonElement.
+        /// Verifies copies are independent from originals.
+        /// </summary>
+        [Fact]
+        public void CanCopyJsonDocumentAndElement()
+        {
             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 jsonString = "{\"name\":\"test\",\"value\":42}";
+            using var doc = JsonDocument.Parse(jsonString);
+
+            // Test JsonElement copy
+            var element = doc.RootElement;
+            var copiedElement = copier.Copy(element);
+            Assert.Equal(element.GetRawText(), copiedElement.GetRawText());
+
+            // Test JsonDocument copy
+            var copiedDocument = copier.Copy(doc);
+            Assert.NotSame(doc, copiedDocument);
+            Assert.Equal(doc.RootElement.GetRawText(), copiedDocument!.RootElement.GetRawText());
 
-            var deserializedObject = copier.Copy(jsonObject);
-            Assert.Equal(System.Text.Json.JsonSerializer.Serialize(jsonObject), System.Text.Json.JsonSerializer.Serialize(deserializedObject));
+            copiedDocument!.Dispose();
         }
     }
 }
diff --git a/test/Tester/JsonNodeGrainTests.cs b/test/Tester/JsonNodeGrainTests.cs
new file mode 100644
index 00000000000..393820fc784
--- /dev/null
+++ b/test/Tester/JsonNodeGrainTests.cs
@@ -0,0 +1,265 @@
+using System;
+using System.Text.Json.Nodes;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Orleans;
+using Orleans.Hosting;
+using Orleans.Serialization;
+using Orleans.TestingHost;
+using TestExtensions;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace DefaultCluster.Tests
+{
+    /// <summary>
+    /// Grain interface for testing JsonNode serialization through Orleans grain calls.
+    /// This interface reproduces the scenario from GitHub issue #9568 where JsonNode parameters
+    /// are used in grain methods, which can receive JsonValue instances at runtime.
+    /// </summary>
+    public interface IJsonNodeTestGrain : IGrainWithIntegerKey
+    {
+        Task<JsonNode> ProcessJsonNode(JsonNode node);
+        Task<string> GetJsonString(JsonNode node);
+    }
+
+    /// <summary>
+    /// Implementation of the JsonNode test grain.
+    /// Provides methods to test serialization and deserialization of JsonNode instances
+    /// through grain method calls, ensuring the fix for issue #9568 works in a distributed context.
+    /// </summary>
+    public class JsonNodeTestGrain : Grain, IJsonNodeTestGrain
+    {
+        public Task<JsonNode> ProcessJsonNode(JsonNode node)
+        {
+            // Simply return the node - serialization should handle it
+            return Task.FromResult(node);
+        }
+
+        public Task<string> GetJsonString(JsonNode node)
+        {
+            // Convert to string to verify we received the correct data
+            return Task.FromResult(node?.ToJsonString() ?? "null");
+        }
+    }
+
+    /// <summary>
+    /// Test fixture that configures an in-process Orleans cluster with JSON serialization support.
+    /// Sets up both silo and client to use the JsonCodec for System.Text.Json types.
+    /// </summary>
+    public class JsonNodeGrainTestFixture : BaseInProcessTestClusterFixture
+    {
+        protected override void ConfigureTestCluster(InProcessTestClusterBuilder builder)
+        {
+            base.ConfigureTestCluster(builder);
+
+            // Configure JSON serialization
+            builder.ConfigureSilo((options, siloBuilder) =>
+            {
+                siloBuilder.Services.AddSerializer(serializerBuilder =>
+                {
+                    serializerBuilder.AddJsonSerializer(_ => false);
+                });
+            });
+
+            builder.ConfigureClient(clientBuilder =>
+            {
+                clientBuilder.Services.AddSerializer(serializerBuilder =>
+                {
+                    serializerBuilder.AddJsonSerializer(_ => false);
+                });
+            });
+        }
+    }
+
+    /// <summary>
+    /// Integration tests for JsonNode serialization in Orleans grains.
+    /// These tests verify that the fix for GitHub issue #9568 works correctly when JsonNode types
+    /// are passed through grain method calls, particularly when JsonValue is passed to a JsonNode parameter.
+    /// </summary>
+    [Trait("Category", "BVT")]
+    public class JsonNodeGrainTests : IClassFixture<JsonNodeGrainTestFixture>
+    {
+        private readonly JsonNodeGrainTestFixture _fixture;
+        private readonly ITestOutputHelper _output;
+
+        public JsonNodeGrainTests(JsonNodeGrainTestFixture fixture, ITestOutputHelper output)
+        {
+            _fixture = fixture;
+            _output = output;
+        }
+
+        /// <summary>
+        /// Reproduces GitHub issue #9568: Tests that a grain method with JsonNode parameter can receive JsonValue.
+        /// This test verifies the core fix where JsonValue (a derived type) is passed to a method expecting JsonNode (base type).
+        /// Previously this would throw InvalidCastException during deserialization.
+        /// </summary>
+        [Fact]
+        public async Task GrainCanProcessJsonValue_AsJsonNode()
+        {
+            // When a grain method has a JsonNode parameter but receives a JsonValue
+            var grain = _fixture.GrainFactory.GetGrain<IJsonNodeTestGrain>(1);
+
+            // Create a JsonValue (which is a subtype of JsonNode)
+            JsonNode jsonNode = JsonValue.Create(1);
+
+            // This should not throw InvalidCastException
+            var result = await grain.ProcessJsonNode(jsonNode);
+
+            // Verify the result
+            Assert.NotNull(result);
+            Assert.Equal(1, result.GetValue<int>());
+        }
+
+        /// <summary>
+        /// Tests that grains can process all JsonNode subtypes (JsonValue, JsonArray, JsonObject).
+        /// Verifies that each type maintains its structure and values through grain method calls.
+        /// </summary>
+        [Fact]
+        public async Task GrainCanProcessVariousJsonNodeTypes()
+        {
+            var grain = _fixture.GrainFactory.GetGrain<IJsonNodeTestGrain>(2);
+
+            // Test JsonValue with int
+            JsonNode intValue = JsonValue.Create(42);
+            var intResult = await grain.ProcessJsonNode(intValue);
+            Assert.Equal(42, intResult.GetValue<int>());
+
+            // Test JsonValue with string
+            JsonNode stringValue = JsonValue.Create("hello");
+            var stringResult = await grain.ProcessJsonNode(stringValue);
+            Assert.Equal("hello", stringResult.GetValue<string>());
+
+            // Test JsonValue with bool
+            JsonNode boolValue = JsonValue.Create(true);
+            var boolResult = await grain.ProcessJsonNode(boolValue);
+            Assert.True(boolResult.GetValue<bool>());
+
+            // Test JsonArray
+            JsonNode arrayValue = new JsonArray { 1, 2, 3 };
+            var arrayResult = await grain.ProcessJsonNode(arrayValue);
+            Assert.IsType<JsonArray>(arrayResult);
+            Assert.Equal(3, ((JsonArray)arrayResult).Count);
+
+            // Test JsonObject
+            JsonNode objectValue = new JsonObject { ["key"] = "value", ["number"] = 123 };
+            var objectResult = await grain.ProcessJsonNode(objectValue);
+            Assert.IsType<JsonObject>(objectResult);
+            Assert.Equal("value", objectResult["key"]!.GetValue<string>());
+            Assert.Equal(123, objectResult["number"]!.GetValue<int>());
+        }
+
+        /// <summary>
+        /// Tests grain processing of complex nested JSON structures.
+        /// Verifies that deeply nested objects containing mixed JsonNode types are correctly
+        /// serialized and deserialized through grain calls.
+        /// </summary>
+        [Fact]
+        public async Task GrainCanProcessComplexJsonStructure()
+        {
+            var grain = _fixture.GrainFactory.GetGrain<IJsonNodeTestGrain>(3);
+
+            // Create a complex nested structure
+            var complexNode = new JsonObject
+            {
+                ["simpleValue"] = JsonValue.Create(123),
+                ["arrayWithValues"] = new JsonArray
+                {
+                    JsonValue.Create("first"),
+                    JsonValue.Create(456),
+                    JsonValue.Create(false)
+                },
+                ["nestedObject"] = new JsonObject
+                {
+                    ["deepValue"] = JsonValue.Create(3.14159),
+                    ["deepArray"] = new JsonArray { JsonValue.Create(true), null }
+                }
+            };
+
+            // Process through grain
+            var result = await grain.ProcessJsonNode(complexNode);
+
+            // Verify structure is preserved
+            Assert.NotNull(result);
+            Assert.Equal(123, result["simpleValue"]!.GetValue<int>());
+
+            var array = result["arrayWithValues"] as JsonArray;
+            Assert.NotNull(array);
+            Assert.Equal("first", array![0]!.GetValue<string>());
+            Assert.Equal(456, array[1]!.GetValue<int>());
+            Assert.False(array[2]!.GetValue<bool>());
+
+            var nested = result["nestedObject"] as JsonObject;
+            Assert.NotNull(nested);
+            Assert.Equal(3.14159, nested!["deepValue"]!.GetValue<double>(), 5);
+        }
+
+        /// <summary>
+        /// Tests that JsonNode instances can be converted to strings within grain methods.
+        /// Verifies that the grain receives valid JsonNode instances that can be manipulated.
+        /// </summary>
+        [Fact]
+        public async Task GrainCanConvertJsonNodeToString()
+        {
+            var grain = _fixture.GrainFactory.GetGrain<IJsonNodeTestGrain>(4);
+
+            // Test with JsonValue
+            JsonNode valueNode = JsonValue.Create(42);
+            var valueString = await grain.GetJsonString(valueNode);
+            Assert.Equal("42", valueString);
+
+            // Test with JsonObject
+            JsonNode objectNode = new JsonObject { ["test"] = "value" };
+            var objectString = await grain.GetJsonString(objectNode);
+            Assert.Contains("\"test\":\"value\"", objectString);
+
+            // Test with JsonArray
+            JsonNode arrayNode = new JsonArray { 1, 2, 3 };
+            var arrayString = await grain.GetJsonString(arrayNode);
+            Assert.Equal("[1,2,3]", arrayString);
+        }
+
+        /// <summary>
+        /// Tests that grain methods correctly handle null JsonNode parameters.
+        /// Ensures that null values are properly serialized and don't cause exceptions.
+        /// </summary>
+        [Fact]
+        public async Task GrainHandlesNullJsonNode()
+        {
+            var grain = _fixture.GrainFactory.GetGrain<IJsonNodeTestGrain>(5);
+
+            JsonNode nullNode = null;
+            var result = await grain.ProcessJsonNode(nullNode);
+            Assert.Null(result);
+
+            var stringResult = await grain.GetJsonString(nullNode);
+            Assert.Equal("null", stringResult);
+        }
+
+        /// <summary>
+        /// Tests thread safety and correctness with concurrent grain calls using JsonNode parameters.
+        /// Verifies that multiple simultaneous grain calls with different JsonNode values don't interfere with each other.
+        /// </summary>
+        [Fact]
+        public async Task MultipleConcurrentGrainCallsWithJsonNodes()
+        {
+            // Test concurrent calls to ensure thread safety
+            var tasks = new Task[10];
+
+            for (int i = 0; i < 10; i++)
+            {
+                var grainId = i + 100;
+                var value = i * 10;
+                tasks[i] = Task.Run(async () =>
+                {
+                    var grain = _fixture.GrainFactory.GetGrain<IJsonNodeTestGrain>(grainId);
+                    JsonNode node = JsonValue.Create(value);
+                    var result = await grain.ProcessJsonNode(node);
+                    Assert.Equal(value, result.GetValue<int>());
+                });
+            }
+
+            await Task.WhenAll(tasks);
+        }
+    }
+}
diff --git a/test/Tester/Tester.csproj b/test/Tester/Tester.csproj
index 86d6c37ae9a..fc3c2377449 100644
--- a/test/Tester/Tester.csproj
+++ b/test/Tester/Tester.csproj
@@ -27,6 +27,7 @@
     <ProjectReference Include="$(SourceRoot)test\Grains\TestFSharp\TestFSharp.fsproj" />
     <ProjectReference Include="$(SourceRoot)src\Orleans.Reminders\Orleans.Reminders.csproj" />
     <ProjectReference Include="$(SourceRoot)src\Orleans.Sdk\Orleans.Sdk.csproj" />
+    <ProjectReference Include="$(SourceRoot)src\Orleans.Serialization.SystemTextJson\Orleans.Serialization.SystemTextJson.csproj" />
     <ProjectReference Include="$(SourceRoot)src\Orleans.BroadcastChannel\Orleans.BroadcastChannel.csproj" />
   </ItemGroup>
 </Project>