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]