This is an automated email from the ASF dual-hosted git repository.
He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git
The following commit(s) were added to refs/heads/main by this push:
new 199e7f77f6 Preserve manifest-less Jackson serialization with
predefined markers (#3666)
199e7f77f6 is described below
commit 199e7f77f6af7b84f701672439b87a5efa40fb75
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Fri Oct 23 17:48:54 2026 +0800
Preserve manifest-less Jackson serialization with predefined markers (#3666)
Motivation:
Predefined marker bindings make existing manifest-less Jackson
configurations fail when they infer a deserialization type from one application
binding.
Modification:
Exclude the predefined fallback markers from type inference in both Jackson
modules. Cover single and multiple application bindings, explicit types, and
marker-only configurations; document the rule.
Result:
Existing single-binding inference works with the new defaults, while
ambiguous or missing application types still fail clearly.
Tests:
- Four JSON/CBOR suites pass: 270 tests on JDK 17; new regressions expose
the issue before the fix.
- Documentation, header, code style, and Jackson 2 MiMa checks pass.
- Jackson 3 has no local MiMa task; full validation runs in CI.
References:
Refs #3649 and
[akka/akka-core#32049](https://github.com/akka/akka-core/pull/32049).
---
docs/src/main/paradox/serialization-jackson.md | 6 ++-
.../serialization/jackson/JacksonSerializer.scala | 5 +-
.../jackson/JacksonSerializerSpec.scala | 57 ++++++++++++++++++++++
.../serialization/jackson3/JacksonSerializer.scala | 5 +-
.../jackson3/JacksonSerializerSpec.scala | 57 ++++++++++++++++++++++
5 files changed, 126 insertions(+), 4 deletions(-)
diff --git a/docs/src/main/paradox/serialization-jackson.md
b/docs/src/main/paradox/serialization-jackson.md
index 0db4bfb3d6..16f5e9f785 100644
--- a/docs/src/main/paradox/serialization-jackson.md
+++ b/docs/src/main/paradox/serialization-jackson.md
@@ -555,8 +555,10 @@ events are small. To address this, a `type-in-manifest`
flag can be turned off,
in the class name not appearing in the manifest.
When deserializing, the Jackson serializer will use the type defined in
`deserialization-type`, if
-present, otherwise it will look for exactly one serialization binding class,
and use that. For
-this to be useful, generally that single type must be a
+present, otherwise it will look for exactly one application serialization
binding class, and use that.
+The predefined `JsonSerializable` and `CborSerializable` marker bindings are
excluded from this inference
+in both Jackson modules; they do not specify a concrete deserialization type.
For
+this to be useful, generally that single type must be a
@ref:[Polymorphic type](#polymorphic-types), with all type information
necessary to deserialize to
the various sub types contained in the JSON message.
diff --git
a/serialization-jackson/src/main/scala/org/apache/pekko/serialization/jackson/JacksonSerializer.scala
b/serialization-jackson/src/main/scala/org/apache/pekko/serialization/jackson/JacksonSerializer.scala
index a7c6815da7..0349acdb9b 100644
---
a/serialization-jackson/src/main/scala/org/apache/pekko/serialization/jackson/JacksonSerializer.scala
+++
b/serialization-jackson/src/main/scala/org/apache/pekko/serialization/jackson/JacksonSerializer.scala
@@ -291,7 +291,10 @@ import pekko.util.OptionVal
None
} else {
configuredDeserializationType.orElse {
- val bindings = serialization.bindings.filter(_._2.identifier ==
identifier)
+ // Predefined markers are fallback bindings, not concrete
deserialization types.
+ val bindings = serialization.bindings.filter { case (clazz, serializer)
=>
+ serializer.identifier == identifier && clazz !=
classOf[JsonSerializable] && clazz != classOf[CborSerializable]
+ }
bindings match {
case Nil =>
throw new IllegalArgumentException(
diff --git
a/serialization-jackson/src/test/scala/org/apache/pekko/serialization/jackson/JacksonSerializerSpec.scala
b/serialization-jackson/src/test/scala/org/apache/pekko/serialization/jackson/JacksonSerializerSpec.scala
index 4f3b3a460e..d9f380ad4b 100644
---
a/serialization-jackson/src/test/scala/org/apache/pekko/serialization/jackson/JacksonSerializerSpec.scala
+++
b/serialization-jackson/src/test/scala/org/apache/pekko/serialization/jackson/JacksonSerializerSpec.scala
@@ -1102,6 +1102,63 @@ abstract class JacksonSerializerSpec(serializerName:
String)
throw new IllegalStateException(s"Wrong serializer ${s.getClass} for
${obj.getClass}")
}
+ "Manifest-less serialization with predefined markers" must {
+ import ScalaTestMessages._
+
+ def withBindings(bindings: String, deserializationType: String = "")(test:
ActorSystem => Unit): Unit = {
+ val config = ConfigFactory.parseString(s"""
+ pekko.actor.serialization-bindings { $bindings }
+ pekko.serialization.jackson.$serializerName {
+ type-in-manifest = off
+ deserialization-type = "$deserializationType"
+ }
+ """).withFallback(ConfigFactory.defaultReference())
+ val sys = ActorSystem("ManifestlessPredefinedMarkers", config)
+ try test(sys)
+ finally shutdown(sys)
+ }
+
+ val firstType = classOf[SimpleCommand].getName
+ val secondType = classOf[SimpleCommand2].getName
+ val oneBinding = s"\"$firstType\" = $serializerName"
+ val twoBindings = s"$oneBinding, \"$secondType\" = $serializerName"
+
+ "infer the single application binding with the default marker enabled" in {
+ withBindings(oneBinding) { sys =>
+ val message = SimpleCommand("one binding")
+ serializerFor(message, sys).manifest(message) should ===("")
+ checkSerialization(message, sys)
+ }
+ }
+
+ "honor an explicit deserialization type with multiple application
bindings" in {
+ withBindings(twoBindings, firstType) { sys =>
+ val message = SimpleCommand("explicit type")
+ serializerFor(message, sys).manifest(message) should ===("")
+ checkSerialization(message, sys)
+ }
+ }
+
+ "reject multiple application bindings without an explicit deserialization
type" in {
+ withBindings(twoBindings) { sys =>
+ val message = SimpleCommand("ambiguous")
+ intercept[IllegalArgumentException] {
+ serializerFor(message, sys).manifest(message)
+ }.getMessage should include("multiple bindings")
+ }
+ }
+
+ "reject a predefined marker as the only inferred deserialization type" in {
+ withBindings("") { sys =>
+ val serializer = serialization(sys).serializerByIdentity(if
(serializerName == "jackson-json") 31 else 33)
+ .asInstanceOf[SerializerWithStringManifest]
+ intercept[IllegalArgumentException] {
+ serializer.manifest(SimpleCommand("no application binding"))
+ }.getMessage should include("none were configured")
+ }
+ }
+ }
+
"JacksonSerializer with Java message classes" must {
import JavaTestMessages._
diff --git
a/serialization-jackson3/src/main/scala/org/apache/pekko/serialization/jackson3/JacksonSerializer.scala
b/serialization-jackson3/src/main/scala/org/apache/pekko/serialization/jackson3/JacksonSerializer.scala
index cf0e1f0f27..4aed2417e6 100644
---
a/serialization-jackson3/src/main/scala/org/apache/pekko/serialization/jackson3/JacksonSerializer.scala
+++
b/serialization-jackson3/src/main/scala/org/apache/pekko/serialization/jackson3/JacksonSerializer.scala
@@ -292,7 +292,10 @@ import pekko.util.OptionVal
None
} else {
configuredDeserializationType.orElse {
- val bindings = serialization.bindings.filter(_._2.identifier ==
identifier)
+ // Predefined markers are fallback bindings, not concrete
deserialization types.
+ val bindings = serialization.bindings.filter { case (clazz, serializer)
=>
+ serializer.identifier == identifier && clazz !=
classOf[JsonSerializable] && clazz != classOf[CborSerializable]
+ }
bindings match {
case Nil =>
throw new IllegalArgumentException(
diff --git
a/serialization-jackson3/src/test/scala/org/apache/pekko/serialization/jackson3/JacksonSerializerSpec.scala
b/serialization-jackson3/src/test/scala/org/apache/pekko/serialization/jackson3/JacksonSerializerSpec.scala
index 9d158d4175..c2ac9c8167 100644
---
a/serialization-jackson3/src/test/scala/org/apache/pekko/serialization/jackson3/JacksonSerializerSpec.scala
+++
b/serialization-jackson3/src/test/scala/org/apache/pekko/serialization/jackson3/JacksonSerializerSpec.scala
@@ -1047,6 +1047,63 @@ abstract class JacksonSerializerSpec(serializerName:
String)
throw new IllegalStateException(s"Wrong serializer ${s.getClass} for
${obj.getClass}")
}
+ "Manifest-less serialization with predefined markers" must {
+ import ScalaTestMessages._
+
+ def withBindings(bindings: String, deserializationType: String = "")(test:
ActorSystem => Unit): Unit = {
+ val config = ConfigFactory.parseString(s"""
+ pekko.actor.serialization-bindings { $bindings }
+ pekko.serialization.jackson3.$serializerName {
+ type-in-manifest = off
+ deserialization-type = "$deserializationType"
+ }
+ """).withFallback(ConfigFactory.defaultReference())
+ val sys = ActorSystem("ManifestlessPredefinedMarkers", config)
+ try test(sys)
+ finally shutdown(sys)
+ }
+
+ val firstType = classOf[SimpleCommand].getName
+ val secondType = classOf[SimpleCommand2].getName
+ val oneBinding = s"\"$firstType\" = $serializerName"
+ val twoBindings = s"$oneBinding, \"$secondType\" = $serializerName"
+
+ "infer the single application binding with the default marker enabled" in {
+ withBindings(oneBinding) { sys =>
+ val message = SimpleCommand("one binding")
+ serializerFor(message, sys).manifest(message) should ===("")
+ checkSerialization(message, sys)
+ }
+ }
+
+ "honor an explicit deserialization type with multiple application
bindings" in {
+ withBindings(twoBindings, firstType) { sys =>
+ val message = SimpleCommand("explicit type")
+ serializerFor(message, sys).manifest(message) should ===("")
+ checkSerialization(message, sys)
+ }
+ }
+
+ "reject multiple application bindings without an explicit deserialization
type" in {
+ withBindings(twoBindings) { sys =>
+ val message = SimpleCommand("ambiguous")
+ intercept[IllegalArgumentException] {
+ serializerFor(message, sys).manifest(message)
+ }.getMessage should include("multiple bindings")
+ }
+ }
+
+ "reject a predefined marker as the only inferred deserialization type" in {
+ withBindings("") { sys =>
+ val serializer = serialization(sys).serializerByIdentity(if
(serializerName == "jackson-json") 31 else 33)
+ .asInstanceOf[SerializerWithStringManifest]
+ intercept[IllegalArgumentException] {
+ serializer.manifest(SimpleCommand("no application binding"))
+ }.getMessage should include("none were configured")
+ }
+ }
+ }
+
"JacksonSerializer with Java message classes" must {
import JavaTestMessages._
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]