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 8d2ec4b97 fix(java): skip missing compatible struct fields (#3833)
8d2ec4b97 is described below
commit 8d2ec4b971d6a39b2f43f48d85e8ea53e58a58c6
Author: Shawn Yang <[email protected]>
AuthorDate: Sun Jul 12 14:04:26 2026 +0530
fix(java): skip missing compatible struct fields (#3833)
## Why?
## What does this PR do?
## Related issues
Closes #3832
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] 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`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence or equivalent persisted links of the
final clean AI review results from both fresh reviewers described in
`AI_POLICY.md`, the Fory-guided reviewer and the independent general
reviewer, on the current PR diff or current HEAD after the latest code
changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
.../fory/builder/CompatibleCodecBuilder.java | 33 ++++++++
.../fory/serializer/AbstractObjectSerializer.java | 12 +++
.../fory/serializer/MetaShareCompatibleTest.java | 89 ++++++++++++++++++++++
.../integration_tests/RecordSerializersTest.java | 51 +++++++++++++
4 files changed, 185 insertions(+)
diff --git
a/java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java
b/java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java
index aa3295fe8..691b7715d 100644
---
a/java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java
+++
b/java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java
@@ -343,6 +343,9 @@ public class CompatibleCodecBuilder extends
ObjectCodecBuilder {
protected Expression deserializeField(
Expression buffer, Descriptor descriptor, Function<Expression,
Expression> callback) {
FieldConverter<?> converter = descriptor.getFieldConverter();
+ if (shouldSkipField(descriptor)) {
+ return skipField(descriptor);
+ }
if (converter == null) {
return super.deserializeField(buffer, descriptor, callback);
}
@@ -353,6 +356,36 @@ public class CompatibleCodecBuilder extends
ObjectCodecBuilder {
return new Expression.ListExpression(targetValue,
callback.apply(targetValue));
}
+ @Override
+ protected Expression deserializeGroupForRecord(
+ List<Descriptor> group, Expression bean, Expression buffer) {
+ Expression.ListExpression expressions = new Expression.ListExpression();
+ for (Descriptor descriptor : group) {
+ if (shouldSkipField(descriptor)) {
+ expressions.add(skipField(descriptor));
+ continue;
+ }
+ TypeRef<?> castTypeRef = readValueTypeRef(descriptor);
+ Expression value = deserializeField(buffer, descriptor, expression ->
expression);
+ expressions.add(setFieldValue(bean, descriptor, tryInlineCast(value,
castTypeRef)));
+ }
+ return expressions;
+ }
+
+ private boolean shouldSkipField(Descriptor descriptor) {
+ return descriptor.getField() == null
+ && descriptor.getFieldConverter() == null
+ && !descriptor.getRawType().isPrimitive();
+ }
+
+ private Expression skipField(Descriptor descriptor) {
+ return new Expression.Invoke(
+ new Expression.Reference("this",
TypeRef.of(GeneratedCompatibleSerializer.class), false),
+ "skipField",
+ readContextRef(),
+ remoteFieldInfo(descriptor));
+ }
+
@Override
protected Expression setFieldValue(Expression bean, Descriptor descriptor,
Expression value) {
if (descriptor.getField() == null) {
diff --git
a/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java
b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java
index a8f00c51e..0681127fa 100644
---
a/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java
+++
b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java
@@ -207,6 +207,18 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
return null;
}
+ protected final void skipField(ReadContext readContext,
SerializationFieldInfo remoteFieldInfo) {
+ // A remote-only struct can have a synthetic UnknownStruct descriptor
while registered type
+ // dispatch still materializes its concrete local class. Generated
compatible readers must use
+ // this untyped path so consuming the field never inserts a cast to the
synthetic type.
+ FieldSkipper.skipField(
+ readContext,
+ typeResolver,
+ readContext.getRefReader(),
+ remoteFieldInfo,
+ readContext.getBuffer());
+ }
+
/**
* Write field value to buffer by reading from the object via fieldAccessor.
Handles primitive
* types, unsigned/compressed numbers, and common types like String with
optimized fast paths.
diff --git
a/java/fory-core/src/test/java/org/apache/fory/serializer/MetaShareCompatibleTest.java
b/java/fory-core/src/test/java/org/apache/fory/serializer/MetaShareCompatibleTest.java
index 85f6a7a00..802fa0854 100644
---
a/java/fory-core/src/test/java/org/apache/fory/serializer/MetaShareCompatibleTest.java
+++
b/java/fory-core/src/test/java/org/apache/fory/serializer/MetaShareCompatibleTest.java
@@ -32,10 +32,13 @@ import java.util.stream.Collectors;
import org.apache.fory.Fory;
import org.apache.fory.ForyTestBase;
import org.apache.fory.TestUtils;
+import org.apache.fory.annotation.ForyField;
+import org.apache.fory.annotation.Nullable;
import org.apache.fory.builder.CompatibleCodecBuilder;
import org.apache.fory.codegen.CompileUnit;
import org.apache.fory.codegen.JaninoUtils;
import org.apache.fory.config.ForyBuilder;
+import org.apache.fory.config.Language;
import org.apache.fory.context.MetaReadContext;
import org.apache.fory.context.MetaWriteContext;
import org.apache.fory.meta.NativeTypeDefEncoderTest;
@@ -92,6 +95,46 @@ public class MetaShareCompatibleTest extends ForyTestBase {
}
}
+ public static final class Address {
+ @Nullable
+ @ForyField(id = 1)
+ public String city;
+
+ @Nullable
+ @ForyField(id = 2)
+ public String zip;
+ }
+
+ public static final class UserV1 {
+ @ForyField(id = 1)
+ public Long id;
+
+ @Nullable
+ @ForyField(id = 2)
+ public String name;
+
+ @Nullable
+ @ForyField(id = 3)
+ public Address homeAddress;
+ }
+
+ public static final class UserV2 {
+ @ForyField(id = 1)
+ public Long id;
+
+ @Nullable
+ @ForyField(id = 2)
+ public String name;
+
+ @Nullable
+ @ForyField(id = 3)
+ public Address homeAddress;
+
+ @Nullable
+ @ForyField(id = 4)
+ public Address workAddress;
+ }
+
@DataProvider
public static Object[][] config1() {
return Sets.cartesianProduct(
@@ -858,6 +901,52 @@ public class MetaShareCompatibleTest extends ForyTestBase {
}
}
+ @Test(dataProvider = "enableCodegen")
+ public void testRegisteredMissingStruct(boolean enableCodegen) {
+ int userTypeId = 100;
+ int addressTypeId = 101;
+ Fory writer =
+ Fory.builder()
+ .withLanguage(Language.JAVA)
+ .withCompatible(true)
+ .withCodegen(enableCodegen)
+ .withAsyncCompilation(false)
+ .requireClassRegistration(true)
+ .build();
+ writer.register(UserV2.class, userTypeId);
+ writer.register(Address.class, addressTypeId);
+
+ UserV2 user = new UserV2();
+ user.id = 1L;
+ user.name = "Alice";
+ Address homeAddress = new Address();
+ homeAddress.city = "NYC";
+ homeAddress.zip = "10001";
+ user.homeAddress = homeAddress;
+ Address workAddress = new Address();
+ workAddress.city = "SF";
+ workAddress.zip = "94000";
+ user.workAddress = workAddress;
+ byte[] bytes = writer.serialize(user);
+
+ Fory reader =
+ Fory.builder()
+ .withLanguage(Language.JAVA)
+ .withCompatible(true)
+ .withCodegen(enableCodegen)
+ .withAsyncCompilation(false)
+ .requireClassRegistration(true)
+ .build();
+ reader.register(UserV1.class, userTypeId);
+ reader.register(Address.class, addressTypeId);
+
+ UserV1 result = (UserV1) reader.deserialize(bytes);
+ Assert.assertEquals(result.id, Long.valueOf(1));
+ Assert.assertEquals(result.name, "Alice");
+ Assert.assertEquals(result.homeAddress.city, "NYC");
+ Assert.assertEquals(result.homeAddress.zip, "10001");
+ }
+
@Test
public void testInconsistentRegistrationID() throws Exception {
ClassLoader classLoader =
diff --git
a/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/RecordSerializersTest.java
b/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/RecordSerializersTest.java
index 53895de84..ebdfbf831 100644
---
a/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/RecordSerializersTest.java
+++
b/java/fory-latest-jdk-tests/src/test/java/org/apache/fory/integration_tests/RecordSerializersTest.java
@@ -30,7 +30,10 @@ import java.util.Arrays;
import java.util.List;
import org.apache.fory.Fory;
import org.apache.fory.ThreadSafeFory;
+import org.apache.fory.annotation.ForyField;
+import org.apache.fory.annotation.Nullable;
import org.apache.fory.config.ForyBuilder;
+import org.apache.fory.config.Language;
import org.apache.fory.context.MetaReadContext;
import org.apache.fory.context.MetaWriteContext;
import org.apache.fory.test.bean.Struct;
@@ -49,6 +52,20 @@ public class RecordSerializersTest {
public record BoxedPrimitiveRecord(String lensId, Long from, Long to,
Integer type)
implements Serializable {}
+ public record AddressRecord(
+ @Nullable @ForyField(id = 1) String city, @Nullable @ForyField(id = 2)
String zip) {}
+
+ public record UserRecordV1(
+ @ForyField(id = 1) Long id,
+ @Nullable @ForyField(id = 2) String name,
+ @Nullable @ForyField(id = 3) AddressRecord homeAddress) {}
+
+ public record UserRecordV2(
+ @ForyField(id = 1) Long id,
+ @Nullable @ForyField(id = 2) String name,
+ @Nullable @ForyField(id = 3) AddressRecord homeAddress,
+ @Nullable @ForyField(id = 4) AddressRecord workAddress) {}
+
@Test
public void testIsRecord() {
Assert.assertTrue(RecordUtils.isRecord(Foo.class));
@@ -93,6 +110,40 @@ public class RecordSerializersTest {
Assert.assertEquals(fory.deserialize(fory.serialize(foo)), foo);
}
+ @Test(dataProvider = "codegen")
+ public void testRegisteredMissingStruct(boolean codegen) {
+ int userTypeId = 100;
+ int addressTypeId = 101;
+ Fory writer =
+ Fory.builder()
+ .withLanguage(Language.JAVA)
+ .withCompatible(true)
+ .withCodegen(codegen)
+ .withAsyncCompilation(false)
+ .requireClassRegistration(true)
+ .build();
+ writer.register(UserRecordV2.class, userTypeId);
+ writer.register(AddressRecord.class, addressTypeId);
+
+ AddressRecord homeAddress = new AddressRecord("NYC", "10001");
+ AddressRecord workAddress = new AddressRecord("SF", "94000");
+ byte[] bytes = writer.serialize(new UserRecordV2(1L, "Alice", homeAddress,
workAddress));
+
+ Fory reader =
+ Fory.builder()
+ .withLanguage(Language.JAVA)
+ .withCompatible(true)
+ .withCodegen(codegen)
+ .withAsyncCompilation(false)
+ .requireClassRegistration(true)
+ .build();
+ reader.register(UserRecordV1.class, userTypeId);
+ reader.register(AddressRecord.class, addressTypeId);
+
+ UserRecordV1 result = (UserRecordV1) reader.deserialize(bytes);
+ Assert.assertEquals(result, new UserRecordV1(1L, "Alice", homeAddress));
+ }
+
@Test
public void testNumberCompressedBoxedLongRecordRoundTripAcrossPools() {
ThreadSafeFory writer = newNumberCompressedPool();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]