Skip to content

Commit 5a697ec

Browse files
authored
[BACKPORT #6503] Fix StackOverflow exception when NewtonsoftJsonSerializer tries to deserialize a JObject inside an object field (#6522)
* Fix `StackOverflow` exception when `NewtonsoftJsonSerializer` tries to deserialize a `JObject` inside an `object` field (#6503) * Reproduction for #6502 * Fix JObject inside object property/field overflow --------- Co-authored-by: Aaron Stannard <[email protected]> (cherry picked from commit 64c6eff) * Remove nullable support
1 parent 3a85e17 commit 5a697ec

File tree

2 files changed

+97
-4
lines changed

2 files changed

+97
-4
lines changed

src/core/Akka.Tests/Serialization/SerializationSpec.cs

+61
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
using System;
99
using System.Collections.Concurrent;
10+
using System.Collections.Generic;
1011
using System.IO;
1112
using System.Reflection;
1213
using System.Runtime.Serialization;
@@ -21,6 +22,7 @@
2122
using Akka.Util;
2223
using Akka.Util.Reflection;
2324
using FluentAssertions;
25+
using Newtonsoft.Json.Linq;
2426
using Xunit;
2527

2628
namespace Akka.Tests.Serialization
@@ -608,6 +610,60 @@ public void Missing_custom_serializer_id_should_append_help_message()
608610
.Where(ex => ex.Message.Contains("Serializer Id [101] is not one of the internal Akka.NET serializer."));
609611
}
610612

613+
[Fact(DisplayName = "Should be able to serialize object property with JObject value")]
614+
public void ObjectPropertyJObjectTest()
615+
{
616+
var serializer = (NewtonSoftJsonSerializer) Sys.Serialization.FindSerializerForType(typeof(object));
617+
var obj = JObject.FromObject(new
618+
{
619+
FormattedMessage = "We are apple 20 points above value 10.01 ms",
620+
Message = "We are {0} {1} points above value {2} ms",
621+
Parameters = new List<object> { "apple", 20, 10.01F, 50L, (decimal) 9.9 },
622+
MessageType = 200
623+
});
624+
var instance = new ObjectTestClass { MyObject = obj};
625+
626+
var serialized = serializer.ToBinary(instance);
627+
628+
// Stack overflowed in the original bug
629+
var deserialized = serializer.FromBinary<ObjectTestClass>(serialized);
630+
deserialized.MyObject.Should().BeOfType<JObject>();
631+
var jObj = (JObject) deserialized.MyObject;
632+
633+
((JValue)jObj["FormattedMessage"]).Value.Should().Be("We are apple 20 points above value 10.01 ms");
634+
((JValue)jObj["Message"]).Value.Should().Be("We are {0} {1} points above value {2} ms");
635+
var arr = ((JArray)jObj["Parameters"]);
636+
((JValue)arr[0]).Value.Should().Be("apple");
637+
((JValue)arr[1]).Value.Should().BeOfType<int>();
638+
((JValue)arr[1]).Value.Should().Be(20);
639+
((JValue)arr[2]).Value.Should().BeOfType<float>();
640+
((JValue)arr[2]).Value.Should().Be(10.01F);
641+
((JValue)arr[3]).Value.Should().BeOfType<long>();
642+
((JValue)arr[3]).Value.Should().Be(50L);
643+
((JValue)arr[4]).Value.Should().BeOfType<decimal>();
644+
((JValue)arr[4]).Value.Should().Be((decimal)9.9);
645+
((JValue)jObj["MessageType"]).Value.Should().Be(200);
646+
}
647+
648+
[Fact(DisplayName = "Should be able to serialize object property with anonymous type value")]
649+
public void ObjectPropertyObjectTest()
650+
{
651+
var serializer = (NewtonSoftJsonSerializer) Sys.Serialization.FindSerializerForType(typeof(object));
652+
var obj = new
653+
{
654+
FormattedMessage = "We are apple 20 points above value 10.01 ms",
655+
Message = "We are {0} {1} points above value {2} ms",
656+
Parameters = new List<object> { "apple", 20, 10.01F, 50L, (decimal) 9.9 },
657+
MessageType = 200
658+
};
659+
var instance = new ObjectTestClass { MyObject = obj};
660+
661+
var serialized = serializer.ToBinary(instance);
662+
663+
var deserialized = serializer.FromBinary<ObjectTestClass>(serialized);
664+
deserialized.MyObject.Should().BeEquivalentTo(obj);
665+
}
666+
611667
public SerializationSpec():base(GetConfig())
612668
{
613669
}
@@ -708,6 +764,11 @@ public sealed class ChildClass
708764
public string Value { get; set; }
709765
}
710766
}
767+
768+
public sealed class ObjectTestClass
769+
{
770+
public object MyObject { get; set; }
771+
}
711772
}
712773
}
713774

src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs

+36-4
Original file line numberDiff line numberDiff line change
@@ -331,8 +331,7 @@ public override object FromBinary(byte[] bytes, Type type)
331331

332332
private static object TranslateSurrogate(object deserializedValue, NewtonSoftJsonSerializer parent, Type type)
333333
{
334-
var j = deserializedValue as JObject;
335-
if (j != null)
334+
if (deserializedValue is JObject j)
336335
{
337336
//The JObject represents a special akka.net wrapper for primitives (int,float,decimal) to preserve correct type when deserializing
338337
if (j["$"] != null)
@@ -341,19 +340,52 @@ private static object TranslateSurrogate(object deserializedValue, NewtonSoftJso
341340
return GetValue(value);
342341
}
343342

343+
// Bug: #6502 Newtonsoft could not deserialize pure JObject inside an object payload.
344+
// If type is `object`, deep-convert object and return as is.
345+
if (type == typeof(object))
346+
{
347+
return RestoreJToken(j);
348+
}
349+
344350
//The JObject is not of our concern, let Json.NET deserialize it.
345351
return j.ToObject(type, parent._serializer);
346352
}
347-
var surrogate = deserializedValue as ISurrogate;
348353

349354
//The deserialized object is a surrogate, unwrap it
350-
if (surrogate != null)
355+
if (deserializedValue is ISurrogate surrogate)
351356
{
352357
return surrogate.FromSurrogate(parent.system);
353358
}
354359
return deserializedValue;
355360
}
356361

362+
private static JToken RestoreJToken(JToken value)
363+
{
364+
switch (value)
365+
{
366+
case JObject obj:
367+
if (obj["$"] != null)
368+
{
369+
var v = obj["$"].Value<string>();
370+
return new JValue(GetValue(v));
371+
}
372+
var dict = (IDictionary<string, JToken>)obj;
373+
foreach (var kvp in dict)
374+
{
375+
dict[kvp.Key] = RestoreJToken(kvp.Value);
376+
}
377+
return obj;
378+
case JArray arr:
379+
for (var i = 0; i < arr.Count; i++)
380+
{
381+
arr[i] = RestoreJToken(arr[i]);
382+
}
383+
return arr;
384+
default:
385+
return value;
386+
}
387+
}
388+
357389
private static object GetValue(string V)
358390
{
359391
var t = V.Substring(0, 1);

0 commit comments

Comments
 (0)