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 8f6a095b7 fix(java): recover map declared serializers for compatible 
reads (#3654)
8f6a095b7 is described below

commit 8f6a095b7161c3c5f28f56d64731c142a332df2e
Author: Sebastian Mandrean <[email protected]>
AuthorDate: Thu May 7 11:48:41 2026 +0200

    fix(java): recover map declared serializers for compatible reads (#3654)
    
    ## Why?
    
    Compatible mode can deserialize newer payloads where an older reader has
    removed a map-subclass field. For map subclasses such as
    `TreeMap<String, String>`, compatible metadata may preserve the skipped
    field as `Map<Object, Object>`, so declared map chunks can be read with
    the wrong key/value serializers and fail while skipping the removed
    field.
    
    Fixes #3652.
    
    ## What does this PR do?
    
    - Recovers declared key/value serializers from the actual map serializer
    when compatible metadata only exposes `Map<Object, Object>`.
    - Applies that recovery to normal map chunks and null-key/null-value
    chunks.
    - Wires the same recovery through generated map readers.
    - Adds a compatible-mode regression covering removed `TreeMap<String,
    String>` subclass fields, including null value and null key chunk cases,
    across async/codegen combinations.
    
    ## Related issues
    
    - Fixes #3652
    
    ## AI Contribution Checklist
    
    
    - [x] Substantial AI assistance was used in this PR: `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`. N/A.
    - [x] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence of the final clean AI review results
    from both fresh reviewers on the current PR diff or current HEAD after
    the latest code changes. N/A.
    
    
    ## Does this PR introduce any user-facing change?
    
    No user-facing behavior change beyond fixing compatible deserialization
    of removed map-subclass fields.
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    This is a reader-side deserialization fix. It does not change map flags,
    writer paths, type ids, metadata layout, or the binary protocol.
    
    ## Benchmark
    
    N/A. This PR does not change serialization format or hot write paths.
    Validation run:
    
    - `mvn -T16 -pl fory-core -am test
    
-Dtest=org.apache.fory.serializer.CompatibleSerializerTest#testCompatibleModeSkipsRemovedTreeMapSubclassField
    -Dsurefire.failIfNoSpecifiedTests=false`
    - `mvn -T16 -pl fory-core -am test
    
-Dtest=org.apache.fory.serializer.CompatibleSerializerTest,org.apache.fory.serializer.collection.MapSerializersTest
    -Dsurefire.failIfNoSpecifiedTests=false`
    - `mvn -T16 -pl fory-core spotless:check checkstyle:check`
    - `python ./ci/run_ci.py java --version graalvm` with local GraalVM 21
    native-image build and binary execution
    
    ---------
    
    Co-authored-by: chaokunyang <[email protected]>
---
 .../main/java/org/apache/fory/meta/FieldTypes.java |  21 ++++-
 .../fory/serializer/CompatibleSerializerTest.java  | 105 +++++++++++++++++++++
 2 files changed, 122 insertions(+), 4 deletions(-)

diff --git a/java/fory-core/src/main/java/org/apache/fory/meta/FieldTypes.java 
b/java/fory-core/src/main/java/org/apache/fory/meta/FieldTypes.java
index 88b53f0e6..12d346e89 100644
--- a/java/fory-core/src/main/java/org/apache/fory/meta/FieldTypes.java
+++ b/java/fory-core/src/main/java/org/apache/fory/meta/FieldTypes.java
@@ -231,6 +231,7 @@ public class FieldTypes {
                   ? GenericType.build(Object.class)
                   : genericType.getTypeParameter0()));
     } else if (MAP_TYPE.isSupertypeOf(genericType.getTypeRef())) {
+      Tuple2<TypeRef<?>, TypeRef<?>> mapKeyValueType = 
getMapKeyValueType(genericType);
       return new MapFieldType(
           typeId,
           nullable,
@@ -238,15 +239,15 @@ public class FieldTypes {
           buildFieldType(
               resolver,
               null, // nested fields don't have Field reference
-              genericType.getTypeParameter0() == null
+              mapKeyValueType.f0 == null
                   ? GenericType.build(Object.class)
-                  : genericType.getTypeParameter0()),
+                  : resolver.buildGenericType(mapKeyValueType.f0)),
           buildFieldType(
               resolver,
               null, // nested fields don't have Field reference
-              genericType.getTypeParameter1() == null
+              mapKeyValueType.f1 == null
                   ? GenericType.build(Object.class)
-                  : genericType.getTypeParameter1()));
+                  : resolver.buildGenericType(mapKeyValueType.f1)));
     } else if (isUnionType || Union.class.isAssignableFrom(rawType)) {
       return new UnionFieldType(nullable, trackingRef);
     } else if (TypeUtils.unwrap(rawType).isPrimitive()) {
@@ -291,6 +292,18 @@ public class FieldTypes {
     }
   }
 
+  private static Tuple2<TypeRef<?>, TypeRef<?>> getMapKeyValueType(GenericType 
genericType) {
+    if (genericType.getTypeParametersCount() >= 2) {
+      return Tuple2.of(
+          genericType.getTypeParameter0().getTypeRef(),
+          genericType.getTypeParameter1().getTypeRef());
+    }
+    if (!MAP_TYPE.isSupertypeOf(genericType.getTypeRef())) {
+      return Tuple2.of(TypeRef.of(Object.class), TypeRef.of(Object.class));
+    }
+    return TypeUtils.getMapKeyValueType(genericType.getTypeRef());
+  }
+
   public abstract static class FieldType implements Serializable {
     private static final int KIND_OBJECT = 0;
     private static final int KIND_MAP = 1;
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/serializer/CompatibleSerializerTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/serializer/CompatibleSerializerTest.java
index 8b0b6556d..004f78b59 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/serializer/CompatibleSerializerTest.java
+++ 
b/java/fory-core/src/test/java/org/apache/fory/serializer/CompatibleSerializerTest.java
@@ -19,14 +19,19 @@
 
 package org.apache.fory.serializer;
 
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.TreeMap;
 import java.util.stream.Collectors;
 import lombok.Data;
 import org.apache.fory.Fory;
 import org.apache.fory.ForyTestBase;
 import org.apache.fory.TestUtils;
+import org.apache.fory.config.CompatibleMode;
+import org.apache.fory.config.Language;
 import org.apache.fory.serializer.collection.UnmodifiableSerializersTest;
 import org.apache.fory.test.bean.BeanA;
 import org.apache.fory.test.bean.BeanB;
@@ -42,6 +47,10 @@ import org.testng.annotations.Test;
  * forward/backward compatibility when using compatible mode with scoped meta 
share.
  */
 public class CompatibleSerializerTest extends ForyTestBase {
+  private static final int CATALOG_SNAPSHOT_CLASS_ID = 4100;
+  private static final int MOVIE_DOCUMENT_CLASS_ID = 4101;
+  private static final int LEGACY_LABELS_CLASS_ID = 4102;
+  private static final int NULLABLE_KEY_LABELS_CLASS_ID = 4103;
 
   @Test
   public void testWrite() {
@@ -242,6 +251,23 @@ public class CompatibleSerializerTest extends ForyTestBase 
{
             CollectionFields.copyToCanEqual(newObj, 
newObj.getClass().newInstance())));
   }
 
+  @Test(dataProvider = "twoBoolOptions")
+  public void testCompatibleModeSkipsRemovedTreeMapSubclassField(
+      boolean asyncCompilation, boolean codegen) {
+    byte[] bytes = writeCatalogSnapshotV1(asyncCompilation, codegen);
+
+    Fory reader = catalogFory(asyncCompilation, codegen);
+    reader.register(CatalogSnapshotV2.class, CATALOG_SNAPSHOT_CLASS_ID);
+    reader.register(MovieDocumentV2.class, MOVIE_DOCUMENT_CLASS_ID);
+    reader.register(LegacyLabelsV2.class, LEGACY_LABELS_CLASS_ID);
+    reader.register(NullableKeyLabelsV2.class, NULLABLE_KEY_LABELS_CLASS_ID);
+
+    CatalogSnapshotV2 decoded = reader.deserialize(bytes, 
CatalogSnapshotV2.class);
+    Assert.assertEquals(decoded.batchId, "batch-1");
+    Assert.assertEquals(decoded.movies.size(), 1);
+    Assert.assertEquals(decoded.movies.get(0).id, "m-1");
+  }
+
   @Test
   public void testWriteCompatibleMap() throws Exception {
     Fory fory =
@@ -340,4 +366,83 @@ public class CompatibleSerializerTest extends ForyTestBase 
{
     byte[] serialized = fory.serialize(beanA);
     Assert.assertEquals(fory.deserialize(serialized, BeanA.class), beanA);
   }
+
+  private static byte[] writeCatalogSnapshotV1(boolean asyncCompilation, 
boolean codegen) {
+    Fory writer = catalogFory(asyncCompilation, codegen);
+    writer.register(CatalogSnapshotV1.class, CATALOG_SNAPSHOT_CLASS_ID);
+    writer.register(MovieDocumentV1.class, MOVIE_DOCUMENT_CLASS_ID);
+    writer.register(LegacyLabelsV1.class, LEGACY_LABELS_CLASS_ID);
+    writer.register(NullableKeyLabelsV1.class, NULLABLE_KEY_LABELS_CLASS_ID);
+    return writer.serialize(new CatalogSnapshotV1("batch-1"));
+  }
+
+  private static Fory catalogFory(boolean asyncCompilation, boolean codegen) {
+    return Fory.builder()
+        .withLanguage(Language.JAVA)
+        .withCompatibleMode(CompatibleMode.COMPATIBLE)
+        .requireClassRegistration(false)
+        .withDeserializeUnknownClass(true)
+        .withAsyncCompilation(asyncCompilation)
+        .withCodegen(codegen)
+        .build();
+  }
+
+  public static class LegacyLabelsV1 extends TreeMap<String, String> {
+    public LegacyLabelsV1() {}
+  }
+
+  public static class LegacyLabelsV2 extends TreeMap<String, String> {
+    public LegacyLabelsV2() {}
+  }
+
+  public static class NullableKeyLabelsV1 extends HashMap<String, String> {
+    public NullableKeyLabelsV1() {}
+  }
+
+  public static class NullableKeyLabelsV2 extends HashMap<String, String> {
+    public NullableKeyLabelsV2() {}
+  }
+
+  public static class MovieDocumentV1 {
+    public String id;
+    public LegacyLabelsV1 labels;
+    public NullableKeyLabelsV1 nullableKeyLabels;
+
+    public MovieDocumentV1() {}
+
+    MovieDocumentV1(String id) {
+      this.id = id;
+      labels = new LegacyLabelsV1();
+      labels.put("region", "se");
+      labels.put("unknown", null);
+      nullableKeyLabels = new NullableKeyLabelsV1();
+      nullableKeyLabels.put(null, "fallback");
+    }
+  }
+
+  public static class MovieDocumentV2 {
+    public String id;
+
+    public MovieDocumentV2() {}
+  }
+
+  public static class CatalogSnapshotV1 {
+    public String batchId;
+    public ArrayList<MovieDocumentV1> movies;
+
+    public CatalogSnapshotV1() {}
+
+    CatalogSnapshotV1(String batchId) {
+      this.batchId = batchId;
+      movies = new ArrayList<>();
+      movies.add(new MovieDocumentV1("m-1"));
+    }
+  }
+
+  public static class CatalogSnapshotV2 {
+    public String batchId;
+    public ArrayList<MovieDocumentV2> movies;
+
+    public CatalogSnapshotV2() {}
+  }
 }


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

Reply via email to