This is an automated email from the ASF dual-hosted git repository.

pjfanning 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 9e28139fb7 fix: fail fast when both Jackson serializer modules are on 
the classpath (#3673)
9e28139fb7 is described below

commit 9e28139fb76e0c3e79aae907526109b136f3ec53
Author: PJ Fanning <[email protected]>
AuthorDate: Fri Oct 23 13:51:43 2026 +0100

    fix: fail fast when both Jackson serializer modules are on the classpath 
(#3673)
    
    * fix: fail fast when both Jackson serializer modules are on the classpath
    
    pekko-serialization-jackson and pekko-serialization-jackson3 register
    serializers with the same names and identifiers. With both on the
    classpath, whichever reference.conf is merged last silently takes over
    the bindings of both modules. Each module's serializer now throws an
    IllegalStateException at construction if the other module's serializer
    class is on the classpath.
    
    Refs #3671
    
    * docs: note in the migration guide that only one Jackson serializer module 
can be used
    
    Refs #3671
    
    * docs: warn against mixing the Jackson serializer modules in a cluster
    
    Refs #3671
---
 .../paradox/migration/migration-guide-1.x-2.x.md   |  7 +++
 docs/src/main/paradox/serialization-jackson.md     | 23 +++++++++-
 .../serialization/jackson/JacksonSerializer.scala  | 18 +++++++-
 .../jackson/JacksonModuleClasspathCheckSpec.scala  | 50 ++++++++++++++++++++++
 .../serialization/jackson3/JacksonSerializer.scala | 18 +++++++-
 .../jackson3/JacksonModuleClasspathCheckSpec.scala | 50 ++++++++++++++++++++++
 6 files changed, 162 insertions(+), 4 deletions(-)

diff --git a/docs/src/main/paradox/migration/migration-guide-1.x-2.x.md 
b/docs/src/main/paradox/migration/migration-guide-1.x-2.x.md
index 59fb37a56f..fa6332d521 100644
--- a/docs/src/main/paradox/migration/migration-guide-1.x-2.x.md
+++ b/docs/src/main/paradox/migration/migration-guide-1.x-2.x.md
@@ -260,3 +260,10 @@ Pekko 1.x nodes during a rolling update. 
([PR3650](https://github.com/apache/pek
 `jackson-cbor`. Your own marker traits and bindings keep working. If you 
switch messages to the
 predefined traits, Pekko 1.x nodes cannot load those classes, so only do that 
once no Pekko 1.x nodes
 remain in the cluster. ([PR3649](https://github.com/apache/pekko/pull/3649))
+* Only one of `pekko-serialization-jackson` and `pekko-serialization-jackson3` 
can be on the
+classpath. Both register serializers with the same names and identifiers, and 
the serializers now fail
+at ActorSystem startup if both modules are present. The two modules have 
different default setups
+(Jackson 2 and Jackson 3 differ in their default features and registered 
modules), so the same message
+can be serialized differently, and data written by one may not be read 
correctly by the other. In a
+Cluster or Remoting setup, make sure all Pekko nodes use the same Jackson 
serializer module.
+([PR3673](https://github.com/apache/pekko/pull/3673))
diff --git a/docs/src/main/paradox/serialization-jackson.md 
b/docs/src/main/paradox/serialization-jackson.md
index 16f5e9f785..5626358994 100644
--- a/docs/src/main/paradox/serialization-jackson.md
+++ b/docs/src/main/paradox/serialization-jackson.md
@@ -43,8 +43,23 @@ The rest of this page applies to both modules, with these 
differences for `pekko
   the Pekko modules, only the Jackson Scala module is registered by default. 
The date/time features such as
   `WRITE_DATES_AS_TIMESTAMPS` are configured in a `datetime-features` section 
instead of `serialization-features`.
 
-Both modules register their serializers with the same names (`jackson-json` 
and `jackson-cbor`) and the
-same serializer identifiers, so only add one of them to your application.
+@@@ warning
+
+Do not use `pekko-serialization-jackson` and `pekko-serialization-jackson3` 
together. Both modules register
+their serializers with the same names (`jackson-json` and `jackson-cbor`) and 
the same serializer identifiers,
+so only add one of them to your application. The serializers fail at 
ActorSystem startup with an
+`IllegalStateException` if both modules are on the classpath.
+
+Do not mix the two modules in a Cluster or Remoting setup either. Jackson 2 
and Jackson 3 have different
+default setups, so the same message can be serialized differently, and data 
written by one module may not be
+read correctly by the other. All Pekko nodes should use the same Jackson 
serializer module. This matters most
+during a @ref:[rolling update](#rolling-updates), when old and new nodes run 
side by side, and when reading
+data that was persisted with the other module.
+
+Moving from `pekko-serialization-jackson` to `pekko-serialization-jackson3` 
needs serious testing of your
+messages and persisted data before you put it into production.
+
+@@@
 
 ## Introduction
 
@@ -419,6 +434,10 @@ To complete a no-degradation rolling update, you need to 
make two deployments. F
 the new schema but still uses the old schema. Then, deploy a second binary 
which serializes data using the new schema
 and drops the downcasting code from the migration.  
 
+Switching between `pekko-serialization-jackson` and 
`pekko-serialization-jackson3` is also a serialization
+change: during a rolling update, nodes using different modules may not be able 
to read each other's messages.
+See the warning in [Jackson 3](#jackson-3).
+
 Let's take, for example, the case above where we [renamed a 
field](#rename-field).
 
 The starting schema is:
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 0349acdb9b..e3819d51b8 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
@@ -27,7 +27,7 @@ import com.fasterxml.jackson.dataformat.cbor.CBORFactory
 import net.jpountz.lz4.LZ4Factory
 
 import org.apache.pekko
-import pekko.actor.ExtendedActorSystem
+import pekko.actor.{ DynamicAccess, ExtendedActorSystem }
 import pekko.annotation.InternalApi
 import pekko.event.{ LogMarker, Logging }
 import pekko.io.UnsynchronizedByteArrayInputStream
@@ -40,6 +40,20 @@ import pekko.util.OptionVal
  */
 @InternalApi private[pekko] object JacksonSerializer {
 
+  private val OtherJacksonModuleSerializerClassName = 
"org.apache.pekko.serialization.jackson3.JacksonSerializer"
+
+  /**
+   * pekko-serialization-jackson and pekko-serialization-jackson3 register 
serializers with the same
+   * names and identifiers, so whichever `reference.conf` is merged last 
silently takes over all bindings
+   * of both modules. Fail fast instead.
+   */
+  def checkOtherJacksonModuleNotOnClasspath(dynamicAccess: DynamicAccess): 
Unit =
+    if 
(dynamicAccess.classIsOnClasspath(OtherJacksonModuleSerializerClassName))
+      throw new IllegalStateException(
+        "pekko-serialization-jackson and pekko-serialization-jackson3 must not 
both be on the classpath. " +
+        "They register serializers with the same names and identifiers, so 
only one of them can be used. " +
+        s"Found [$OtherJacksonModuleSerializerClassName]. Remove one of them 
from your dependencies.")
+
   /**
    * Using the deny list from Jackson databind of class names that shouldn't 
be allowed.
    * Not nice to depend on implementation details of Jackson, but good to use 
the same
@@ -220,6 +234,8 @@ import pekko.util.OptionVal
     extends SerializerWithStringManifest {
   import JacksonSerializer._
 
+  checkOtherJacksonModuleNotOnClasspath(system.dynamicAccess)
+
   // TODO issue #27107: it should be possible to implement 
ByteBufferSerializer as well, using Jackson's
   //      ByteBufferBackedOutputStream/ByteBufferBackedInputStream
 
diff --git 
a/serialization-jackson/src/test/scala/org/apache/pekko/serialization/jackson/JacksonModuleClasspathCheckSpec.scala
 
b/serialization-jackson/src/test/scala/org/apache/pekko/serialization/jackson/JacksonModuleClasspathCheckSpec.scala
new file mode 100644
index 0000000000..eb59790062
--- /dev/null
+++ 
b/serialization-jackson/src/test/scala/org/apache/pekko/serialization/jackson/JacksonModuleClasspathCheckSpec.scala
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pekko.serialization.jackson
+
+import org.apache.pekko
+import pekko.actor.ReflectiveDynamicAccess
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+class JacksonModuleClasspathCheckSpec extends AnyWordSpec with Matchers {
+
+  private val otherModuleSerializer = 
"org.apache.pekko.serialization.jackson3.JacksonSerializer"
+
+  private def dynamicAccess(otherModuleOnClasspath: Boolean) =
+    new ReflectiveDynamicAccess(getClass.getClassLoader) {
+      override def classIsOnClasspath(fqcn: String): Boolean =
+        if (fqcn == otherModuleSerializer) otherModuleOnClasspath else 
super.classIsOnClasspath(fqcn)
+    }
+
+  "The Jackson serializer classpath check" should {
+    "pass when the other Jackson serializer module is not on the classpath" in 
{
+      new 
ReflectiveDynamicAccess(getClass.getClassLoader).classIsOnClasspath(otherModuleSerializer)
 should ===(false)
+      
JacksonSerializer.checkOtherJacksonModuleNotOnClasspath(dynamicAccess(otherModuleOnClasspath
 = false))
+    }
+
+    "fail when the other Jackson serializer module is on the classpath" in {
+      val e = intercept[IllegalStateException] {
+        
JacksonSerializer.checkOtherJacksonModuleNotOnClasspath(dynamicAccess(otherModuleOnClasspath
 = true))
+      }
+      e.getMessage should include("must not both be on the classpath")
+      e.getMessage should include(otherModuleSerializer)
+    }
+  }
+}
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 4aed2417e6..307561d762 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
@@ -27,7 +27,7 @@ import tools.jackson.databind.jsontype.impl.SubTypeValidator
 import tools.jackson.dataformat.cbor.CBORFactory
 
 import org.apache.pekko
-import pekko.actor.ExtendedActorSystem
+import pekko.actor.{ DynamicAccess, ExtendedActorSystem }
 import pekko.annotation.InternalApi
 import pekko.event.{ LogMarker, Logging }
 import pekko.io.UnsynchronizedByteArrayInputStream
@@ -40,6 +40,20 @@ import pekko.util.OptionVal
  */
 @InternalApi private[pekko] object JacksonSerializer {
 
+  private val OtherJacksonModuleSerializerClassName = 
"org.apache.pekko.serialization.jackson.JacksonSerializer"
+
+  /**
+   * pekko-serialization-jackson and pekko-serialization-jackson3 register 
serializers with the same
+   * names and identifiers, so whichever `reference.conf` is merged last 
silently takes over all bindings
+   * of both modules. Fail fast instead.
+   */
+  def checkOtherJacksonModuleNotOnClasspath(dynamicAccess: DynamicAccess): 
Unit =
+    if 
(dynamicAccess.classIsOnClasspath(OtherJacksonModuleSerializerClassName))
+      throw new IllegalStateException(
+        "pekko-serialization-jackson and pekko-serialization-jackson3 must not 
both be on the classpath. " +
+        "They register serializers with the same names and identifiers, so 
only one of them can be used. " +
+        s"Found [$OtherJacksonModuleSerializerClassName]. Remove one of them 
from your dependencies.")
+
   /**
    * Using the deny list from Jackson databind of class names that shouldn't 
be allowed.
    * Not nice to depend on implementation details of Jackson, but good to use 
the same
@@ -221,6 +235,8 @@ import pekko.util.OptionVal
     extends SerializerWithStringManifest {
   import JacksonSerializer._
 
+  checkOtherJacksonModuleNotOnClasspath(system.dynamicAccess)
+
   // TODO issue #27107: it should be possible to implement 
ByteBufferSerializer as well, using Jackson's
   //      ByteBufferBackedOutputStream/ByteBufferBackedInputStream
 
diff --git 
a/serialization-jackson3/src/test/scala/org/apache/pekko/serialization/jackson3/JacksonModuleClasspathCheckSpec.scala
 
b/serialization-jackson3/src/test/scala/org/apache/pekko/serialization/jackson3/JacksonModuleClasspathCheckSpec.scala
new file mode 100644
index 0000000000..647442530e
--- /dev/null
+++ 
b/serialization-jackson3/src/test/scala/org/apache/pekko/serialization/jackson3/JacksonModuleClasspathCheckSpec.scala
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pekko.serialization.jackson3
+
+import org.apache.pekko
+import pekko.actor.ReflectiveDynamicAccess
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+class JacksonModuleClasspathCheckSpec extends AnyWordSpec with Matchers {
+
+  private val otherModuleSerializer = 
"org.apache.pekko.serialization.jackson.JacksonSerializer"
+
+  private def dynamicAccess(otherModuleOnClasspath: Boolean) =
+    new ReflectiveDynamicAccess(getClass.getClassLoader) {
+      override def classIsOnClasspath(fqcn: String): Boolean =
+        if (fqcn == otherModuleSerializer) otherModuleOnClasspath else 
super.classIsOnClasspath(fqcn)
+    }
+
+  "The Jackson serializer classpath check" should {
+    "pass when the other Jackson serializer module is not on the classpath" in 
{
+      new 
ReflectiveDynamicAccess(getClass.getClassLoader).classIsOnClasspath(otherModuleSerializer)
 should ===(false)
+      
JacksonSerializer.checkOtherJacksonModuleNotOnClasspath(dynamicAccess(otherModuleOnClasspath
 = false))
+    }
+
+    "fail when the other Jackson serializer module is on the classpath" in {
+      val e = intercept[IllegalStateException] {
+        
JacksonSerializer.checkOtherJacksonModuleNotOnClasspath(dynamicAccess(otherModuleOnClasspath
 = true))
+      }
+      e.getMessage should include("must not both be on the classpath")
+      e.getMessage should include(otherModuleSerializer)
+    }
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to