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]