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]

Reply via email to