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

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new e85b857c5 fix(java): Correct resolution of dependent/nested 
serializers in GraalVM (#3532)
e85b857c5 is described below

commit e85b857c506dfa84e6f148969bca3cd0ee531e54
Author: rakow <[email protected]>
AuthorDate: Wed Apr 1 09:24:32 2026 +0200

    fix(java): Correct resolution of dependent/nested serializers in GraalVM 
(#3532)
    
    ## Why?
    
    GraalVM native-image serializer codegen can leak
    `GraalvmSerializerHolder` into generated serializer constructor paths.
    When serializer resolution returns `serializer.getClass()` instead of
    the underlying generated serializer class, dependent generated codecs
    can later fail with
    
    `
    java.lang.ClassCastException: […]Codec_0 cannot be cast to
    org.apache.fory.util.GraalvmSupport$GraalvmSerializerHolder
            at org.apache.fory.Fory.processSerializationError(Fory.java:377)
    `
    
    or
    
    `java.lang.RuntimeException: Class […] is not registered`
    
    This PR fixes that resolver behavior and adds regression tests.
    
    ## What does this PR do?
    
    - Fixes `ClassResolver` to unwrap holder-backed serializers via
    `getGraalvmSerializerClass(...)` when resolving serializer classes.
    - Fixes `TypeResolver` to query the iterated resolver, not `this`, when
    checking the GraalVM registry, and to unwrap holder-backed serializers
    there as well.
    - Adds a new GraalVM integration example,
    `CompatibleDependentSerializerThreadSafeExample`, that features
    generated serializer to depend on another
    
    ## Related issues
    
    None.
    
    ## AI Contribution Checklist
    
    - [x] Substantial AI assistance was used in this PR: `yes` / `no`
    - [x] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [x] If `yes`, I included the standardized `AI Usage Disclosure` block
    below.
    - [x] If `yes`, I can explain and defend all important changes without
    AI help.
    - [x] If `yes`, I reviewed AI-assisted code changes line by line before
    submission.
    - [x] If `yes`, I ran adequate human verification and recorded evidence.
    - [x] If `yes`, I added/updated tests and specs where required.
    - [x] If `yes`, I validated protocol/performance impacts with evidence
    when applicable.
    - [x] If `yes`, I verified licensing and provenance compliance.
    
    ```text
    AI Usage Disclosure
    - substantial_ai_assistance: yes
    - scope: code drafting, regression tests
    - affected_files_or_subsystems: java/fory-core resolver logic; 
integration_tests/graalvm_tests GraalVM regression example
    - human_verification: contributor reproduced the error using tests and than 
verified the fix works as expected, contributor reviewed changes line by line
    - performance_verification: no intended user-facing performance change and 
no dedicated benchmark was run
    - provenance_license_confirmation: Apache-2.0-compatible provenance 
confirmed; no incompatible third-party code introduced
    ```
    
    ## Benchmark
    
    N/A. No dedicated benchmark was run for this change. The validation for
    this PR is targeted regression coverage for GraalVM build-time
    serializer resolution and GraalVM integration behavior.
---
 .../CompatibleDependentSerializerExample.java      | 230 +++++++++++++++++++++
 .../main/java/org/apache/fory/graalvm/Main.java    |   1 +
 .../graalvm_tests/native-image.properties          |   1 +
 .../org/apache/fory/resolver/ClassResolver.java    |   2 +-
 .../org/apache/fory/resolver/TypeResolver.java     |   4 +-
 5 files changed, 235 insertions(+), 3 deletions(-)

diff --git 
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/CompatibleDependentSerializerExample.java
 
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/CompatibleDependentSerializerExample.java
new file mode 100644
index 000000000..7d45c9214
--- /dev/null
+++ 
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/CompatibleDependentSerializerExample.java
@@ -0,0 +1,230 @@
+/*
+ * 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.fory.graalvm;
+
+import java.util.List;
+import java.util.Objects;
+import org.apache.fory.Fory;
+import org.apache.fory.config.CompatibleMode;
+import org.apache.fory.util.Preconditions;
+
+/**
+ * Regression example for GraalVM compatible serializer resolution when one 
generated serializer
+ * depends on another generated serializer.
+ */
+public class CompatibleDependentSerializerExample {
+  static Fory fory;
+
+  static {
+    fory = createFory();
+  }
+
+  private static Fory createFory() {
+    Fory fory =
+        Fory.builder()
+            .withName(CompatibleDependentSerializerExample.class.getName())
+            .requireClassRegistration(true)
+            .withCompatibleMode(CompatibleMode.COMPATIBLE)
+            .build();
+    fory.register(Payload.class);
+    fory.register(LongPayload.class);
+    fory.register(TextPayload.class);
+    fory.register(NestedPayload.class);
+    fory.register(ChildPayload.class);
+    fory.register(ParentEnvelope.class);
+    fory.ensureSerializersCompiled();
+    return fory;
+  }
+
+  public static void main(String[] args) {
+    test(fory);
+    System.out.println("CompatibleDependentSerializerExample succeed");
+
+    // TODO: Recreating the fory instance exposes an issue with
+    // getMetaSharedDeserializerClassFromGraalvmRegistry
+    // fory = createFory();
+    // test(fory);
+    // System.out.println("CompatibleDependentSerializerExample succeed");
+  }
+
+  static void test(Fory fory) {
+    ParentEnvelope envelope = newEnvelope();
+    Object result = fory.deserialize(fory.serialize(envelope));
+    Preconditions.checkArgument(envelope.equals(result), "Round-trip should 
preserve envelope");
+  }
+
+  private static ParentEnvelope newEnvelope() {
+    LongPayload longPayload = new LongPayload();
+    longPayload.label = "long";
+    longPayload.value = 42L;
+
+    TextPayload textPayload = new TextPayload();
+    textPayload.label = "text";
+    textPayload.text = "payload-9";
+
+    NestedPayload nestedPayload = new NestedPayload();
+    nestedPayload.code = "nested";
+    nestedPayload.payload = textPayload;
+
+    ChildPayload childPayload = new ChildPayload();
+    childPayload.name = "child";
+    childPayload.nested = nestedPayload;
+    childPayload.payloads = List.of(longPayload, textPayload);
+
+    ParentEnvelope envelope = new ParentEnvelope();
+    envelope.id = 42;
+    envelope.child = childPayload;
+    envelope.primaryPayload = textPayload;
+    envelope.payloads = List.of(textPayload, longPayload);
+    return envelope;
+  }
+
+  /** Root object for the regression scenario with nested and polymorphic 
payload fields. */
+  public static class ParentEnvelope {
+    public int id;
+    public ChildPayload child;
+    public Payload primaryPayload;
+    public List<Payload> payloads;
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      ParentEnvelope that = (ParentEnvelope) o;
+      return id == that.id
+          && Objects.equals(child, that.child)
+          && Objects.equals(primaryPayload, that.primaryPayload)
+          && Objects.equals(payloads, that.payloads);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(id, child, primaryPayload, payloads);
+    }
+  }
+
+  public static class ChildPayload {
+    public String name;
+    public NestedPayload nested;
+    public List<Payload> payloads;
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      ChildPayload that = (ChildPayload) o;
+      return Objects.equals(name, that.name)
+          && Objects.equals(nested, that.nested)
+          && Objects.equals(payloads, that.payloads);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(name, nested, payloads);
+    }
+  }
+
+  public abstract static class Payload {
+    public String label;
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      Payload payload = (Payload) o;
+      return Objects.equals(label, payload.label);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(label);
+    }
+  }
+
+  public static final class LongPayload extends Payload {
+    public long value;
+
+    @Override
+    public boolean equals(Object o) {
+      if (!super.equals(o)) {
+        return false;
+      }
+      LongPayload that = (LongPayload) o;
+      return value == that.value;
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(super.hashCode(), value);
+    }
+  }
+
+  public static final class TextPayload extends Payload {
+    public String text;
+
+    @Override
+    public boolean equals(Object o) {
+      if (!super.equals(o)) {
+        return false;
+      }
+      TextPayload that = (TextPayload) o;
+      return Objects.equals(text, that.text);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(super.hashCode(), text);
+    }
+  }
+
+  public static final class NestedPayload {
+    public String code;
+    public Payload payload;
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      NestedPayload that = (NestedPayload) o;
+      return Objects.equals(code, that.code) && Objects.equals(payload, 
that.payload);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(code, payload);
+    }
+  }
+}
diff --git 
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
 
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
index 52631e4ed..d84cf6561 100644
--- 
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
+++ 
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
@@ -35,6 +35,7 @@ public class Main {
 
     ThreadSafeExample.main(args);
     CompatibleThreadSafeExample.main(args);
+    CompatibleDependentSerializerExample.main(args);
     ProxyExample.main(args);
     ObjectStreamExample.main(args);
     EnsureSerializerExample.main(args);
diff --git 
a/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties
 
b/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties
index fa2b13ad1..c62793701 100644
--- 
a/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties
+++ 
b/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties
@@ -27,6 +27,7 @@ Args=-H:+ReportExceptionStackTraces \
   org.apache.fory.graalvm.record.RecordExample2,\
   org.apache.fory.graalvm.ThreadSafeExample,\
   org.apache.fory.graalvm.CompatibleThreadSafeExample,\
+  org.apache.fory.graalvm.CompatibleDependentSerializerExample,\
   org.apache.fory.graalvm.ProxyExample,\
   org.apache.fory.graalvm.ObjectStreamExample,\
   org.apache.fory.graalvm.EnsureSerializerExample,\
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
index a3034fc1e..27d336a30 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
@@ -1194,7 +1194,7 @@ public class ClassResolver extends TypeResolver {
     if (typeInfo != null && typeInfo.serializer != null) {
       // Note: need to check `classInfo.serializer != null`, because sometimes 
`cls` is already
       // serialized, which will create a class info with serializer null, see 
`#writeClassInternal`
-      return typeInfo.serializer.getClass();
+      return getGraalvmSerializerClass(typeInfo.serializer);
     } else {
       if (getSerializerFactory() != null) {
         Serializer serializer = getSerializerFactory().createSerializer(fory, 
cls);
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
index 8e172300b..8adac5e2a 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
@@ -1584,9 +1584,9 @@ public abstract class TypeResolver {
     }
     for (TypeResolver resolver : resolvers) {
       if (resolver != this) {
-        TypeInfo typeInfo = getTypeInfo(cls, false);
+        TypeInfo typeInfo = resolver.getTypeInfo(cls, false);
         if (typeInfo != null && typeInfo.serializer != null) {
-          return typeInfo.serializer.getClass();
+          return resolver.getGraalvmSerializerClass(typeInfo.serializer);
         }
       }
     }


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

Reply via email to