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 b6b18aaa2 fix(java): honor record field encoding in generated decode 
(#3626)
b6b18aaa2 is described below

commit b6b18aaa29051c60595e5b4e15ca7914f29f634b
Author: Sebastian Mandrean <[email protected]>
AuthorDate: Tue Apr 28 13:49:14 2026 +0200

    fix(java): honor record field encoding in generated decode (#3626)
    
    ## Why?
    
    Java record deserialization in generated compatible-mode serializers
    used type-level primitive decoding for nullable record fields. For boxed
    primitive record components this could make the generated writer and
    reader choose different encodings, corrupting values during round trip.
    
    This fixes the shared root cause for #3622 and #3624.
    
    ## What does this PR do?
    
    - Adds a `ThreadSafeFory` cross-pool reproducer for a boxed `Long`
    record with number/string compression.
    - Adds a compatible-mode codegen reproducer for a record with boxed
    `Long` and `Integer` components matching #3622.
    - Decodes nullable record fields through descriptor-aware generated
    field paths so read encoding matches write encoding.
    - Makes generated float/double literals locale-stable with
    `Locale.ROOT`.
    
    ## Related issues
    
    - Fixes #3622
    - Fixes #3624
    
    ## AI Contribution Checklist
    
    - [x] Substantial AI assistance was used in this PR: `yes`
    - [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`.
    - [ ] 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.
    
    Contributor checklist for substantial AI assistance:
    
    - [x] Substantial AI assistance was used in this PR: `yes`
    - [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 completed line-by-line self-review first and fixed
    issues before requesting AI review.
    - [x] If `yes`, I ran two fresh AI review agents on the current PR diff
    or current HEAD after the latest code changes: one using
    `.claude/skills/fory-code-review/SKILL.md` and one without that skill.
    - [ ] If `yes`, I addressed all AI review comments and repeated the
    review loop until both AI reviewers reported no further actionable
    comments.
    - [ ] If `yes`, I attached 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 in this PR body.
    - [x] If `yes`, I ran adequate local verification and recorded evidence
    (checks run locally or in CI, pass/fail summary, and confirmation I
    reviewed results).
    - [x] If `yes`, I added/updated tests and specs where required.
    - [ ] If `yes`, I validated protocol/performance impacts with evidence
    when applicable.
    - [x] If `yes`, I verified licensing and provenance compliance.
    
    AI Usage Disclosure
    
    - substantial_ai_assistance: yes
    - scope: code drafting, test drafting, PR description drafting
    - affected_files_or_subsystems: Java generated object codec/read path,
    Java record latest-JDK tests, generated literal codegen
    - ai_review: Pending final contributor line-by-line review and required
    two-reviewer AI review loop before maintainer review.
    - ai_review_artifacts: Pending; final clean review screenshots or
    persistent links have not yet been attached.
    - human_verification: Pending contributor review. Local verification was
    run in this workspace; each command and result is listed below.
    - PASS: `mvn -pl fory-latest-jdk-tests -am
    
-Dtest=org.apache.fory.integration_tests.RecordSerializersTest#testCompatibleCodegenBoxedPrimitiveRecordRoundTrip
    -Dsurefire.failIfNoSpecifiedTests=false test`
    - PASS: `mvn -pl fory-latest-jdk-tests -am
    -Dtest=org.apache.fory.integration_tests.RecordSerializersTest
    -Dsurefire.failIfNoSpecifiedTests=false test`
    - PASS: `ENABLE_FORY_DEBUG_OUTPUT=1 mvn -pl fory-latest-jdk-tests -am
    
-Dtest=org.apache.fory.integration_tests.RecordSerializersTest,org.apache.fory.integration_tests.RecordXlangTest
    -Dsurefire.failIfNoSpecifiedTests=false test`
    - PASS: `ENABLE_FORY_DEBUG_OUTPUT=1 mvn -pl fory-core -am
    
-Dtest=org.apache.fory.serializer.CodegenSerializerTest,org.apache.fory.builder.ObjectCodecBuilderTest
    -Dsurefire.failIfNoSpecifiedTests=false test`
    - PASS: `mvn -pl fory-core,fory-latest-jdk-tests -DskipTests
    spotless:check checkstyle:check` (Maven reported cached metadata
    warnings for `global-maven-virtual`, but the build completed
    successfully.)
    - performance_verification: No benchmark run; this is a targeted Java
    generated-code bug fix. No public API or binary protocol compatibility
    change is intended.
    - provenance_license_confirmation: Locally-authored changes only; no
    third-party code introduced.
    
    ## 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
    
    N/A. This is a targeted Java generated-code bug fix; no benchmark was
    run.
---
 .../fory/builder/BaseObjectCodecBuilder.java       |  6 +--
 .../java/org/apache/fory/codegen/Expression.java   |  7 ++-
 .../integration_tests/RecordSerializersTest.java   | 59 ++++++++++++++++++++++
 3 files changed, 66 insertions(+), 6 deletions(-)

diff --git 
a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
 
b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
index e7aee5ee6..a05527725 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
@@ -1856,17 +1856,15 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
     } else {
       if (typeRef.isPrimitive() && !nullable) {
         // Only skip null check if BOTH: local type is primitive AND sender 
didn't write null flag
-        Expression value = deserializeForNotNull(buffer, typeRef, null);
+        Expression value = deserializeForNotNullForField(buffer, descriptor, 
null);
         // Should put value expr ahead to avoid generated code in wrong scope.
         return new ListExpression(value, callback.apply(value));
       }
-      // Pass local field type so readNullable can use default value for 
primitives when null
-      Class<?> localFieldType = typeRef.isPrimitive() ? typeRef.getRawType() : 
null;
       return readNullableField(
           buffer,
           descriptor,
           callback,
-          () -> deserializeForNotNull(buffer, typeRef, null),
+          () -> deserializeForNotNullForField(buffer, descriptor, null),
           nullable);
     }
   }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java 
b/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java
index 25ce0b294..e1311a4c8 100644
--- a/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java
+++ b/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java
@@ -56,6 +56,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.apache.fory.memory.Platform;
@@ -430,7 +431,8 @@ public interface Expression {
             return new ExprCode(
                 FalseLiteral, new LiteralValue(javaType, 
"Float.NEGATIVE_INFINITY"));
           } else {
-            return new ExprCode(FalseLiteral, new LiteralValue(javaType, 
String.format("%fF", f)));
+            return new ExprCode(
+                FalseLiteral, new LiteralValue(javaType, 
String.format(Locale.ROOT, "%fF", f)));
           }
         } else if (javaType == Double.class) {
           Double d = (Double) value;
@@ -443,7 +445,8 @@ public interface Expression {
             return new ExprCode(
                 FalseLiteral, new LiteralValue(javaType, 
"Double.NEGATIVE_INFINITY"));
           } else {
-            return new ExprCode(FalseLiteral, new LiteralValue(javaType, 
String.format("%fD", d)));
+            return new ExprCode(
+                FalseLiteral, new LiteralValue(javaType, 
String.format(Locale.ROOT, "%fD", d)));
           }
         } else if (javaType == Byte.class) {
           return new ExprCode(
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 ff54a6ad3..71a4f515e 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
@@ -22,13 +22,17 @@ package org.apache.fory.integration_tests;
 import static org.apache.fory.collection.Collections.ofArrayList;
 import static org.apache.fory.collection.Collections.ofHashMap;
 
+import java.io.Serializable;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import org.apache.fory.Fory;
+import org.apache.fory.ThreadSafeFory;
 import org.apache.fory.config.CompatibleMode;
+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;
@@ -42,6 +46,11 @@ public class RecordSerializersTest {
 
   public record Foo(int f1, String f2, List<String> f3, char f4) {}
 
+  public record NumberCompressedPayload(Long longValue, String stringValue) {}
+
+  public record BoxedPrimitiveRecord(String lensId, Long from, Long to, 
Integer type)
+      implements Serializable {}
+
   @Test
   public void testIsRecord() {
     Assert.assertTrue(RecordUtils.isRecord(Foo.class));
@@ -80,6 +89,56 @@ public class RecordSerializersTest {
     Assert.assertEquals(fory.deserialize(fory.serialize(foo)), foo);
   }
 
+  @Test
+  public void testNumberCompressedBoxedLongRecordRoundTripAcrossPools() {
+    ThreadSafeFory writer = newNumberCompressedPool();
+    ThreadSafeFory reader = newNumberCompressedPool();
+
+    NumberCompressedPayload payload =
+        new NumberCompressedPayload(123_456_789L, "longer string with 
multibyte: \u00ff\u00fe");
+
+    byte[] bytes = writer.serialize(payload);
+    Assert.assertEquals(reader.deserialize(bytes), payload);
+  }
+
+  @Test
+  public void testCompatibleCodegenBoxedPrimitiveRecordRoundTrip() {
+    Fory fory =
+        Fory.builder()
+            .withLanguage(Language.JAVA)
+            .requireClassRegistration(false)
+            .withCompatibleMode(CompatibleMode.COMPATIBLE)
+            .withClassVersionCheck(true)
+            .withCodegen(true)
+            .build();
+    BoxedPrimitiveRecord record =
+        new BoxedPrimitiveRecord(
+            
"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:11111111-2222-3333-4444-555555555555",
+            123456789012345L,
+            98765432109876L,
+            146);
+    Assert.assertEquals(fory.deserialize(fory.serialize(record)), record);
+  }
+
+  private static ThreadSafeFory newNumberCompressedPool() {
+    return newNumberCompressedBuilder().buildThreadSafeForyPool(4);
+  }
+
+  private static ForyBuilder newNumberCompressedBuilder() {
+    return Fory.builder()
+        .withLanguage(Language.JAVA)
+        .withCodegen(true)
+        .withAsyncCompilation(false)
+        .requireClassRegistration(false)
+        .suppressClassRegistrationWarnings(true)
+        .withDeserializeUnknownClass(true)
+        .withRefTracking(true)
+        .withCompatibleMode(CompatibleMode.COMPATIBLE)
+        .withStringCompressed(true)
+        .withNumberCompressed(true)
+        .withRefCopy(true);
+  }
+
   @Test(dataProvider = "codegen")
   public void testSimpleRecordMetaShared(boolean codegen) {
     Fory fory =


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

Reply via email to