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 3fc76a4fe feat(java): optimize java serialization perf (#3794)
3fc76a4fe is described below

commit 3fc76a4fe7f8ef68db080a608367e415cb7ce4c5
Author: Shawn Yang <[email protected]>
AuthorDate: Mon Jun 29 12:13:53 2026 +0530

    feat(java): optimize java serialization perf (#3794)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## Related issues
    
    
    
    ## 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
---
 .agents/ci-and-pr.md                               |   6 +-
 .agents/languages/java.md                          |  15 ++-
 .github/workflows/build-containerized-pr.yml       |   3 +
 .github/workflows/build-containerized-release.yml  |   3 +
 .github/workflows/build-native-pr.yml              |   3 +
 .github/workflows/build-native-release.yml         |   3 +
 .github/workflows/ci.yml                           |   5 +-
 .github/workflows/clean-pr-body.yml                |   3 +
 .github/workflows/lint.yml                         |   3 +
 .github/workflows/pr-lint.yml                      |   3 +
 .github/workflows/release-compiler.yaml            |   3 +
 .github/workflows/release-csharp.yaml              |   3 +
 .github/workflows/release-dart.yaml                |   3 +
 .github/workflows/release-java-snapshot.yaml       |   3 +
 .github/workflows/release-javascript.yaml          |   3 +
 .github/workflows/release-python.yaml              |   3 +
 .github/workflows/release-rust.yaml                |   3 +
 .github/workflows/sync.yml                         |   3 +
 docs/compiler/generated-code.md                    |   3 +-
 .../integration_tests/JpmsFieldAccessorTest.java   |   5 +-
 .../src/main/java/org/apache/fory/Fory.java        |   4 +-
 .../fory/builder/BaseObjectCodecBuilder.java       | 135 ++++++++++++++++++---
 .../java/org/apache/fory/builder/CodecBuilder.java |   3 +
 .../java/org/apache/fory/builder/CodecUtils.java   |  47 ++-----
 .../apache/fory/builder/ObjectCodecBuilder.java    |   5 +-
 .../org/apache/fory/codegen/CodeGenerator.java     |  80 +-----------
 .../org/apache/fory/codegen/CodegenContext.java    |  19 +--
 .../java/org/apache/fory/codegen/CompileUnit.java  |  18 ---
 .../apache/fory/codegen/ExpressionOptimizer.java   |  12 +-
 .../java/org/apache/fory/context/ReadContext.java  |  18 +++
 .../java/org/apache/fory/context/WriteContext.java |  28 +++++
 .../java/org/apache/fory/memory/MemoryBuffer.java  |   4 +-
 .../org/apache/fory/resolver/ClassResolver.java    |  52 ++++----
 .../org/apache/fory/resolver/TypeResolver.java     | 124 ++++++++++++++-----
 .../org/apache/fory/serializer/EnumSerializer.java |  16 ++-
 .../apache/fory/serializer/StringSerializer.java   |  17 +--
 .../apache/fory/serializer/UnionSerializer.java    |  10 +-
 .../fory/serializer/UnknownClassSerializers.java   |  16 ++-
 .../org/apache/fory/memory/MemoryBuffer.java       |   4 +-
 .../org/apache/fory/builder/CodecUtilsTest.java    |  15 ---
 .../fory/serializer/StringSerializerTest.java      |  35 ++++++
 java/fory-json/pom.xml                             |  54 +++++++++
 .../org/apache/fory/json/codegen/JsonCodegen.java  |  23 ++--
 .../org/apache/fory/json/ForyJsonTestModels.java   |   3 +-
 44 files changed, 524 insertions(+), 297 deletions(-)

diff --git a/.agents/ci-and-pr.md b/.agents/ci-and-pr.md
index 6f7171f09..6bf0672ac 100644
--- a/.agents/ci-and-pr.md
+++ b/.agents/ci-and-pr.md
@@ -132,7 +132,11 @@ implementation, CI-fix, or verification task, or when the 
user explicitly asks f
 
 ## Workflow Changes
 
-- In ASF GitHub Actions, verify new `uses:` entries against approved Apache 
infrastructure action patterns; replace unapproved actions with approved 
actions plus shell or Python logic when needed.
+- In ASF GitHub Actions, verify new or updated `uses:` entries against the 
approved Apache
+  infrastructure action reference at
+  `https://github.com/apache/infrastructure-actions/blob/main/actions.yml`; 
use an allowlisted pin
+  from that file or replace unapproved actions with approved actions plus 
shell or Python logic when
+  needed.
 - Do not add workflow dependencies on new repository variables or secrets when 
GitHub Actions context, constants, or checked-in configuration can provide 
stable non-secret values. Coordinate required repo config before landing if no 
safe default exists.
 - If a setup action fails because of a runtime deprecation, verify the current 
official major and upgrade to the compatible major rather than adding temporary 
runner flags.
 - Reusable release automation belongs under `ci/` unless the user explicitly 
requests a language-local script.
diff --git a/.agents/languages/java.md b/.agents/languages/java.md
index 3f4b16512..41b19b206 100644
--- a/.agents/languages/java.md
+++ b/.agents/languages/java.md
@@ -205,15 +205,14 @@ Load this file when changing anything under `java/` or 
when Java drives a cross-
   overlay only to call `Lookup#defineHiddenClass` directly, and do not move it 
to `java9` because
   `Lookup#defineClass` defines normal package classes, not hidden nestmates. 
Root code must avoid
   direct `Lookup.ClassOption` linkage and cache the method-handle/option-array 
setup off the hot
-  path.
-- Hidden generated serializers are Java25+ only. Do not broaden serializer 
hidden-class definition
-  to Java15-24, because those runtimes still use the unsafe-backed 
field/object path. Keep
-  `AccessorHelper` as the source-generated same-package helper; do not turn it 
into a bytecode
+  path. Keep this method available for future Java25+ designs even when 
runtime codegen does not
+  call it.
+- Runtime generated serializers use the normal `CodeGenerator` classloader 
path, not hidden
+  nestmate class definition. Janino-generated source still cannot directly 
access target bean
+  private fields through hidden nestmate definition, so do not reintroduce 
hidden serializer loading
+  or same-package source-access plumbing without a separate Java25 design and 
measured proof.
+  `AccessorHelper` remains the source-generated same-package helper; do not 
turn it into a bytecode
   hidden-field owner unless a separate Java25-only design explicitly requires 
that.
-- JDK25 hidden generated serializers must not emit private split helper 
methods. Janino lowers
-  private instance helpers to static bridge methods whose receiver parameter 
uses the original
-  binary class name, which fails hidden-class verification. Use non-private 
final split helpers on
-  that path.
 - Runtime codegen must not emit Janino source that names bootstrap JDK 
implementation classes in
   concealed or non-source-public packages. Generated source in the unnamed 
module cannot access
   those classes even when Fory's trusted field-access path can read/write 
their fields; use
diff --git a/.github/workflows/build-containerized-pr.yml 
b/.github/workflows/build-containerized-pr.yml
index 55d516324..6434f2cf5 100644
--- a/.github/workflows/build-containerized-pr.yml
+++ b/.github/workflows/build-containerized-pr.yml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: Build Containerized PR Wheels
 on:
   pull_request:
diff --git a/.github/workflows/build-containerized-release.yml 
b/.github/workflows/build-containerized-release.yml
index 6e6899144..e8b1bc7c7 100644
--- a/.github/workflows/build-containerized-release.yml
+++ b/.github/workflows/build-containerized-release.yml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: Build Containerized Release Wheels
 on:
   push:
diff --git a/.github/workflows/build-native-pr.yml 
b/.github/workflows/build-native-pr.yml
index 6c7227a75..e45cd26d4 100644
--- a/.github/workflows/build-native-pr.yml
+++ b/.github/workflows/build-native-pr.yml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: Build Native PR Wheels
 on:
   pull_request:
diff --git a/.github/workflows/build-native-release.yml 
b/.github/workflows/build-native-release.yml
index fac625cd2..9098ade6a 100644
--- a/.github/workflows/build-native-release.yml
+++ b/.github/workflows/build-native-release.yml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: Build Native Release Wheels
 on:
   push:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5c770366e..0207528a0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: Fory CI
 
 concurrency:
@@ -535,7 +538,7 @@ jobs:
         java-version: ["17", "21", "25"]
     steps:
       - uses: actions/checkout@v5
-      - uses: graalvm/setup-graalvm@f744c72a42b1995d7b0cbc314bde4bace7ac1fe1 # 
1.5.0
+      - uses: graalvm/setup-graalvm@6f3fa030c4b8f77c1f554a860f593a654538fa38 # 
1.5.6
         with:
           java-version: ${{ matrix.java-version }}
           distribution: "graalvm"
diff --git a/.github/workflows/clean-pr-body.yml 
b/.github/workflows/clean-pr-body.yml
index a90312f73..5961c9323 100644
--- a/.github/workflows/clean-pr-body.yml
+++ b/.github/workflows/clean-pr-body.yml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: "Clean PR Body"
 
 on:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 67a573f5d..b64a3ad4f 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: ❄️ Lint
 
 on:
diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml
index bbd4c57fa..c330f6ac0 100644
--- a/.github/workflows/pr-lint.yml
+++ b/.github/workflows/pr-lint.yml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: "Lint PR"
 
 on:
diff --git a/.github/workflows/release-compiler.yaml 
b/.github/workflows/release-compiler.yaml
index d155c180a..096e4c1c1 100644
--- a/.github/workflows/release-compiler.yaml
+++ b/.github/workflows/release-compiler.yaml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: Publish Compiler
 run-name: "Compiler Release: ${{ github.ref_name }}"
 
diff --git a/.github/workflows/release-csharp.yaml 
b/.github/workflows/release-csharp.yaml
index 1f58e544c..8f458f46e 100644
--- a/.github/workflows/release-csharp.yaml
+++ b/.github/workflows/release-csharp.yaml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: Publish C#
 run-name: "C# Release: ${{ github.ref_name }}"
 
diff --git a/.github/workflows/release-dart.yaml 
b/.github/workflows/release-dart.yaml
index 8525237af..1191d8544 100644
--- a/.github/workflows/release-dart.yaml
+++ b/.github/workflows/release-dart.yaml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: Publish Dart
 run-name: "Dart Release: ${{ github.ref_name }}"
 
diff --git a/.github/workflows/release-java-snapshot.yaml 
b/.github/workflows/release-java-snapshot.yaml
index af869e9eb..d82dcc6a0 100644
--- a/.github/workflows/release-java-snapshot.yaml
+++ b/.github/workflows/release-java-snapshot.yaml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: Publish Fory Java Snapshot
 
 on:
diff --git a/.github/workflows/release-javascript.yaml 
b/.github/workflows/release-javascript.yaml
index a2883ebcf..f0c43db2e 100644
--- a/.github/workflows/release-javascript.yaml
+++ b/.github/workflows/release-javascript.yaml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: Publish JavaScript
 run-name: "JavaScript Release: ${{ github.ref_name }}"
 
diff --git a/.github/workflows/release-python.yaml 
b/.github/workflows/release-python.yaml
index f09ac43d2..5c480eeac 100644
--- a/.github/workflows/release-python.yaml
+++ b/.github/workflows/release-python.yaml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: Publish Python
 run-name: "Python Release: ${{ contains(github.event.workflow_run.name, 
'Containerized') && 'Containerized' || 'Native' }} (Run ID: ${{ 
github.event.workflow_run.id }})"
 
diff --git a/.github/workflows/release-rust.yaml 
b/.github/workflows/release-rust.yaml
index 9f1153bb4..9ce88548f 100644
--- a/.github/workflows/release-rust.yaml
+++ b/.github/workflows/release-rust.yaml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: Publish Rust
 run-name: "Rust Release: ${{ github.ref_name }}"
 
diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml
index a920ef65d..543b0ed13 100644
--- a/.github/workflows/sync.yml
+++ b/.github/workflows/sync.yml
@@ -15,6 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# All `uses:` action pins in this workflow must come from the Apache action 
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
 name: Sync Files
 
 on:
diff --git a/docs/compiler/generated-code.md b/docs/compiler/generated-code.md
index b3e5ebc91..68fddf234 100644
--- a/docs/compiler/generated-code.md
+++ b/docs/compiler/generated-code.md
@@ -1062,8 +1062,7 @@ export enum AnimalCase {
 }
 
 export type Animal =
-  | { case: AnimalCase.DOG; value: Dog }
-  | { case: AnimalCase.CAT; value: Cat };
+  { case: AnimalCase.DOG; value: Dog } | { case: AnimalCase.CAT; value: Cat };
 ```
 
 ### Schema Helpers
diff --git 
a/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java
 
b/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java
index 5abb72c25..3f81ea9c7 100644
--- 
a/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java
+++ 
b/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java
@@ -130,9 +130,8 @@ public class JpmsFieldAccessorTest {
     Assert.assertEquals(result.value(), 17);
 
     Class<?> serializerClass = serializerClass(fory, PrivateFieldBean.class);
-    Assert.assertTrue((Boolean) 
Class.class.getMethod("isHidden").invoke(serializerClass));
-    Assert.assertSame(
-        Class.class.getMethod("getNestHost").invoke(serializerClass), 
PrivateFieldBean.class);
+    // Runtime codegen uses the normal CodeGenerator classloader path. JDK25+ 
final-field access is
+    // guaranteed by the generated VarHandle field, not by hidden-nestmate 
source access.
     assertVarHandleField(serializerClass, "value");
   }
 
diff --git a/java/fory-core/src/main/java/org/apache/fory/Fory.java 
b/java/fory-core/src/main/java/org/apache/fory/Fory.java
index 5ad083d69..a7916400a 100644
--- a/java/fory-core/src/main/java/org/apache/fory/Fory.java
+++ b/java/fory-core/src/main/java/org/apache/fory/Fory.java
@@ -352,7 +352,7 @@ public final class Fory implements BaseFory {
         if (writeContext.getDepth() > 0) {
           throwDepthSerializationException();
         }
-        writeContext.writeRef(obj);
+        writeContext.writeRootRef(obj);
         return buffer;
       } catch (Throwable t) {
         throw processSerializationError(t);
@@ -512,7 +512,7 @@ public final class Fory implements BaseFory {
         if (readContext.getDepth() > 0) {
           throwDepthDeserializationException();
         }
-        return readContext.readRef();
+        return readContext.readRootRef();
       } finally {
         jitContext.unlock();
       }
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 c431f7ec7..a5cbb9c6e 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
@@ -138,6 +138,7 @@ import 
org.apache.fory.serializer.PrimitiveSerializers.LongSerializer;
 import org.apache.fory.serializer.ReplaceResolveSerializer;
 import org.apache.fory.serializer.Serializer;
 import org.apache.fory.serializer.StringSerializer;
+import org.apache.fory.serializer.UnknownClassSerializers;
 import org.apache.fory.serializer.collection.CollectionFlags;
 import org.apache.fory.serializer.collection.CollectionLikeSerializer;
 import org.apache.fory.serializer.collection.CollectionSerializer;
@@ -220,10 +221,6 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
     descriptorDispatchId = new HashMap<>();
   }
 
-  void setSamePackageAccess(boolean samePackageAccess) {
-    ctx.setSamePackageAccess(samePackageAccess);
-  }
-
   // Must be static to be shared across the whole process life.
   private static final Map<String, Map<String, Integer>> idGenerator = new 
ConcurrentHashMap<>();
 
@@ -701,6 +698,18 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
       Expression inputObject, Expression buffer, Descriptor descriptor, 
Expression serializer) {
     TypeRef<?> typeRef = descriptor.getTypeRef();
     Class<?> clz = getRawType(typeRef);
+    if (isEnumType(clz)) {
+      Expression enumSerializer = serializer == null ? 
getSerializerForField(clz) : serializer;
+      if (hasEnumValueMethods(enumSerializer)) {
+        Class<?> serializerClass = getRawType(enumSerializer.type());
+        Expression value =
+            EnumSerializer.class.isAssignableFrom(serializerClass)
+                ? cast(inputObject, TypeRef.of(Enum.class))
+                : inputObject;
+        return new Invoke(enumSerializer, "writeValue", writeContextRef(), 
buffer, value);
+      }
+      return new Invoke(enumSerializer, writeMethodName, writeContextRef(), 
inputObject);
+    }
     if (serializer != null) {
       return new Invoke(serializer, writeMethodName, writeContextRef(), 
inputObject);
     }
@@ -717,6 +726,17 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
     return getOrCreateSerializer(cls, true);
   }
 
+  private static boolean isEnumType(Class<?> cls) {
+    return cls != Enum.class && Enum.class.isAssignableFrom(cls);
+  }
+
+  private static boolean hasEnumValueMethods(Expression serializer) {
+    Class<?> serializerClass = getRawType(serializer.type());
+    // UnknownEnumSerializer is enum-shaped but does not extend EnumSerializer.
+    return EnumSerializer.class.isAssignableFrom(serializerClass)
+        || 
UnknownClassSerializers.UnknownEnumSerializer.class.isAssignableFrom(serializerClass);
+  }
+
   protected Expression serializeForNullable(
       Expression inputObject, Expression buffer, TypeRef<?> typeRef, boolean 
nullable) {
     return serializeForNullable(inputObject, buffer, typeRef, null, false, 
nullable);
@@ -885,6 +905,7 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
     Class<?> clz = getRawType(typeRef);
     Expression clsExpr = new Invoke(inputObject, "getClass", "cls", 
CLASS_TYPE);
     ListExpression writeClassAndObject = new ListExpression();
+    Expression exactClassWrite = exactClassWrite(inputObject, buffer, clz);
     Tuple2<Reference, Boolean> classInfoRef = addTypeInfoField(clz);
     Expression classInfo = classInfoRef.f0;
     if (classInfoRef.f1) {
@@ -904,12 +925,25 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
             PRIMITIVE_VOID_TYPE,
             writeContextRef(),
             inputObject));
+    Expression write =
+        exactClassWrite == null
+            ? writeClassAndObject
+            : new If(eq(clsExpr, getClassExpr(clz)), exactClassWrite, 
writeClassAndObject, false);
     return invokeGenerated(
-        ctx,
-        writeCutPoints(buffer, inputObject),
-        writeClassAndObject,
-        "writeClassAndObject",
-        false);
+        ctx, writeCutPoints(buffer, inputObject), write, 
"writeClassAndObject", false);
+  }
+
+  private Expression exactClassWrite(Expression inputObject, Expression 
buffer, Class<?> clz) {
+    if (clz.isInterface() || Modifier.isAbstract(clz.getModifiers())) {
+      return null;
+    }
+    Reference typeInfo = addExactTypeInfoField(clz);
+    Expression serializer = getOrCreateSerializer(clz);
+    return new ListExpression(
+        typeResolver(
+            r -> r.writeExactClassExpr(typeResolverRef, writeContextRef(), 
buffer, typeInfo, clz)),
+        new Invoke(
+            serializer, writeMethodName, PRIMITIVE_VOID_TYPE, 
writeContextRef(), inputObject));
   }
 
   protected Expression writeTypeInfo(
@@ -1128,6 +1162,21 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
     return classInfoRef;
   }
 
+  protected Reference addExactTypeInfoField(Class<?> cls) {
+    String key = "exactClassInfo:" + cls;
+    Reference reference = (Reference) sharedFieldMap.get(key);
+    if (reference != null) {
+      return reference;
+    }
+    Expression classInfoExpr =
+        inlineInvoke(typeResolverRef, "getTypeInfo", classInfoTypeRef, 
getClassExpr(cls));
+    String name = ctx.newName(ctx.newName(cls) + "ExactTypeInfo");
+    ctx.addField(true, ctx.type(TypeInfo.class), name, classInfoExpr);
+    reference = fieldRef(name, classInfoTypeRef);
+    sharedFieldMap.put(key, reference);
+    return reference;
+  }
+
   protected Reference addTypeInfoHolderField(Class<?> cls) {
     // Final type need to write classinfo when meta share enabled.
     String key;
@@ -1334,11 +1383,18 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
         builder.add(
             writeContainerElements(elementType, true, null, null, buffer, 
collection, size));
       } else {
+        Expression declSameNoNull =
+            eq(flags, ofInt(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL), 
"declSameNoNull");
         Literal hasNullFlag = ofInt(CollectionFlags.HAS_NULL);
         Expression hasNull = eq(new BitAnd(flags, hasNullFlag), hasNullFlag, 
"hasNull");
         builder.add(
+            declSameNoNull,
             hasNull,
-            writeContainerElements(elementType, false, null, hasNull, buffer, 
collection, size));
+            new If(
+                declSameNoNull,
+                writeContainerElements(elementType, false, null, null, buffer, 
collection, size),
+                writeContainerElements(elementType, false, null, hasNull, 
buffer, collection, size),
+                false));
       }
     } else {
       Literal flag = ofInt(CollectionFlags.IS_SAME_TYPE);
@@ -1398,9 +1454,28 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
                 differentTypeWrite);
       } else {
         // if declared elem type don't track ref, all elements must not write 
ref.
+        Expression declSameNoNull =
+            eq(flags, ofInt(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL), 
"declSameNoNull");
         Literal hasNullFlag = ofInt(CollectionFlags.HAS_NULL);
         Expression hasNull = eq(new BitAnd(flags, hasNullFlag), hasNullFlag, 
"hasNull");
-        builder.add(hasNull);
+        builder.add(declSameNoNull, hasNull);
+        Expression declaredNoNullWrite = null;
+        if (maybeDecl) {
+          declaredNoNullWrite =
+              invokeGenerated(
+                  ctx,
+                  writeCutPoints(buffer, collection, size),
+                  writeContainerElements(
+                      elementType,
+                      false,
+                      cast(getOrCreateSerializer(elemClass), serializerType),
+                      null,
+                      buffer,
+                      collection,
+                      size),
+                  "declSameNoNullWrite",
+                  false);
+        }
         ListExpression writeBuilder = new ListExpression(elemSerializer);
         writeBuilder.add(
             writeContainerElements(
@@ -1415,6 +1490,9 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
                 invokeGenerated(ctx, cutPoint, writeBuilder, 
"sameElementClassWrite", false),
                 writeContainerElements(
                     elementType, false, null, hasNull, buffer, collection, 
size));
+        if (declaredNoNullWrite != null) {
+          action = new If(declSameNoNull, declaredNoNullWrite, action, false);
+        }
       }
       builder.add(action);
     }
@@ -2344,6 +2422,16 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
         return StringSerializer.readStringExpr(
             getOrCreateStringSerializer(), buffer, config.compressString());
       }
+      if (isEnumType(cls)) {
+        Expression enumSerializer = serializer == null ? 
getSerializerForField(cls) : serializer;
+        if (!hasEnumValueMethods(enumSerializer)) {
+          return cast(
+              new Invoke(enumSerializer, readMethodName, OBJECT_TYPE, 
readContextRef()),
+              TypeRef.of(Enum.class));
+        }
+        return new Invoke(
+            enumSerializer, "readValue", TypeRef.of(Enum.class), 
readContextRef(), buffer);
+      }
       Expression obj;
       if (usesPrimitiveListArrayProtocol(descriptor)) {
         serializer = getPrimitiveListArraySerializer(cls);
@@ -2595,7 +2683,7 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
           classInfo = readTypeInfo(getRawType(typeRef), buffer);
         }
       } else {
-        classInfo = readTypeInfo(getRawType(typeRef), buffer);
+        classInfo = readTypeInfo(rawType, buffer);
       }
       serializer = inlineInvoke(classInfo, "getSerializer", SERIALIZER_TYPE);
     }
@@ -2638,16 +2726,20 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
     Expression collection =
         new Invoke(serializer, "newCollection", COLLECTION_TYPE, 
readContextRef);
     Expression size = new Invoke(serializer, "getAndClearNumElements", "size", 
PRIMITIVE_INT_TYPE);
-    // if add branch by `ArrayList`, generated code will be > 325 bytes.
-    // and List#add is more likely be inlined if there is only one subclass.
+    // Do not add an ArrayList-specific branch here: it pushes generated code 
over 325 bytes, and
+    // List#add is more likely to inline when the call site has only one 
receiver subclass.
     Expression hookRead = readCollectionCodegen(buffer, collection, size, 
elementType);
     hookRead = new Invoke(serializer, "onCollectionRead", OBJECT_TYPE, 
hookRead);
-    Expression action =
-        new If(
-            supportHook,
-            new ListExpression(collection, hookRead),
-            read(serializer, buffer, OBJECT_TYPE),
+    Expression fallbackAction = read(serializer, buffer, OBJECT_TYPE);
+    Expression fallbackRead =
+        invokeGenerated(
+            ctx,
+            readCutPoints(buffer, serializer),
+            new ListExpression(fallbackAction, new Return(fallbackAction)),
+            "readCollectionFallback",
             false);
+    Expression action =
+        new If(supportHook, new ListExpression(collection, hookRead), 
fallbackRead, false);
     if (invokeHint != null && invokeHint.genNewMethod) {
       invokeHint.add(buffer);
       invokeHint.add(readContextRef());
@@ -2664,7 +2756,10 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
   protected Expression readCollectionCodegen(
       Expression buffer, Expression collection, Expression size, TypeRef<?> 
elementType) {
     ListExpression builder = new ListExpression();
-    Invoke flags = new Invoke(buffer, "readByte", "flags", PRIMITIVE_INT_TYPE, 
false);
+    // The flags byte is the next encoded byte. Use the checked read path so 
truncated
+    // buffer-backed or stream-backed input fails before advancing the reader 
index.
+    Expression flags =
+        cast(new Invoke(buffer, "readByte", PRIMITIVE_BYTE_TYPE), 
PRIMITIVE_INT_TYPE);
     builder.add(flags);
     Class<?> elemClass = TypeUtils.getRawType(elementType);
     walkPath.add(elementType.toString());
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java 
b/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java
index 7bfd4898c..8e5083b6c 100644
--- a/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java
@@ -518,6 +518,8 @@ public abstract class CodecBuilder {
     if (descriptor.getTypeRef().isPrimitive()) {
       Preconditions.checkArgument(getRawType(value.type()) == 
getRawType(fieldType));
     }
+    // Janino cannot compile VarHandle's signature-polymorphic access modes 
directly. Keep generated
+    // serializers on typed helper methods even though the VarHandle itself is 
a static final field.
     return new StaticInvoke(
         varHandleSupportClass(),
         varHandleSetMethod(fieldType),
@@ -609,6 +611,7 @@ public abstract class CodecBuilder {
   private Expression varHandleGetField(Expression inputObject, Descriptor 
descriptor) {
     TypeRef<?> returnType =
         descriptor.getTypeRef().isPrimitive() ? descriptor.getTypeRef() : 
OBJECT_TYPE;
+    // See varHandleSetField: direct VarHandle access-mode calls are not valid 
Janino output.
     Expression getValue =
         new StaticInvoke(
             varHandleSupportClass(),
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java 
b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
index 2361179dd..c9092f672 100644
--- a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
@@ -19,6 +19,7 @@
 
 package org.apache.fory.builder;
 
+import java.util.Collections;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
 import org.apache.fory.Fory;
@@ -26,10 +27,7 @@ import org.apache.fory.codegen.CodeGenerator;
 import org.apache.fory.codegen.CompileUnit;
 import org.apache.fory.collection.Tuple3;
 import org.apache.fory.meta.TypeDef;
-import org.apache.fory.platform.AndroidSupport;
 import org.apache.fory.platform.GraalvmSupport;
-import org.apache.fory.platform.JdkVersion;
-import org.apache.fory.reflect.ReflectionUtils;
 import org.apache.fory.reflect.TypeRef;
 import org.apache.fory.resolver.TypeResolver;
 import org.apache.fory.serializer.Serializer;
@@ -120,6 +118,12 @@ public class CodecUtils {
   @SuppressWarnings("unchecked")
   static <T> Class<? extends Serializer> loadOrGenCodecClass(
       Class<T> beanClass, Fory fory, BaseObjectCodecBuilder codecBuilder) {
+    // use genCodeFunc to avoid gen code repeatedly
+    CompileUnit compileUnit =
+        new CompileUnit(
+            CodeGenerator.getPackage(beanClass),
+            codecBuilder.codecClassName(beanClass),
+            codecBuilder::genCode);
     CodeGenerator codeGenerator;
     ClassLoader beanClassClassLoader =
         beanClass.getClassLoader() == null
@@ -130,40 +134,15 @@ public class CodecUtils {
     }
     TypeResolver typeResolver = fory.getTypeResolver();
     codeGenerator = getCodeGenerator(beanClassClassLoader, typeResolver);
-    Class<?> neighborClass = codecNeighbor(beanClass, beanClassClassLoader);
-    codecBuilder.setSamePackageAccess(neighborClass != null);
-    // use genCodeFunc to avoid gen code repeatedly
-    CompileUnit compileUnit =
-        new CompileUnit(
-            CodeGenerator.getPackage(beanClass),
-            codecBuilder.codecClassName(beanClass),
-            codecBuilder::genCode,
-            neighborClass);
-    return (Class<? extends Serializer>)
-        codeGenerator.compileAndLoad(compileUnit, compileState -> 
compileState.lock.lock());
-  }
-
-  private static Class<?> codecNeighbor(Class<?> beanClass, ClassLoader 
beanClassClassLoader) {
-    // Hidden generated serializers are only a JDK25+ path. JDK8-24 keeps the 
existing unsafe-backed
-    // field/object access strategy, so broadening this to Java15+ adds 
complexity without removing
-    // unsafe from those runtimes.
-    if (AndroidSupport.IS_ANDROID
-        || JdkVersion.MAJOR_VERSION < 25
-        || beanClass.getClassLoader() == null) {
-      return null;
-    }
-    if 
(!CodeGenerator.getPackage(beanClass).equals(ReflectionUtils.getPackage(beanClass)))
 {
-      return null;
-    }
+    ClassLoader classLoader =
+        codeGenerator.compile(
+            Collections.singletonList(compileUnit), compileState -> 
compileState.lock.lock());
+    String className = codecBuilder.codecQualifiedClassName(beanClass);
     try {
-      // A generated serializer defined in the bean loader must resolve Fory 
runtime classes there.
-      if (beanClassClassLoader.loadClass(Fory.class.getName()) == Fory.class) {
-        return beanClass;
-      }
+      return (Class<? extends Serializer>) classLoader.loadClass(className);
     } catch (ClassNotFoundException e) {
-      // The composed-loader path remains the owner when the bean loader 
cannot see Fory directly.
+      throw new IllegalStateException("Impossible because we just compiled 
class", e);
     }
-    return null;
   }
 
   private static CodeGenerator getCodeGenerator(
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java 
b/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java
index e22cd2b38..b467361ac 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java
@@ -799,9 +799,10 @@ public class ObjectCodecBuilder extends 
BaseObjectCodecBuilder {
     Expression bean;
     if (!isRecord) {
       bean = newBean();
-      Expression referenceObject = invokeReadContext("reference", bean);
       expressions.add(bean);
-      expressions.add(referenceObject);
+      if (typeResolver.trackingRef()) {
+        expressions.add(invokeReadContext("reference", bean));
+      }
     } else {
       if (recordCtrAccessible) {
         bean = new FieldsCollector();
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java 
b/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java
index f63e4151c..826029a4c 100644
--- a/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java
+++ b/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java
@@ -40,7 +40,6 @@ import org.apache.fory.logging.Logger;
 import org.apache.fory.logging.LoggerFactory;
 import org.apache.fory.platform.AndroidSupport;
 import org.apache.fory.platform.GraalvmSupport;
-import org.apache.fory.platform.internal.DefineClass;
 import org.apache.fory.reflect.ReflectionUtils;
 import org.apache.fory.util.ClassLoaderUtils;
 import org.apache.fory.util.ClassLoaderUtils.ByteArrayClassLoader;
@@ -136,46 +135,6 @@ public class CodeGenerator {
       }
       parentClassLoader = classLoader;
     }
-    Map<String, byte[]> classes = compileToBytecode(compileUnits, 
parentClassLoader, callback);
-    return defineClasses(classes);
-  }
-
-  public Class<?> compileAndLoad(CompileUnit unit, CompileCallback callback) {
-    Class<?> neighborClass = unit.getNeighborClass();
-    if (neighborClass == null) {
-      ClassLoader loader = compile(java.util.Collections.singletonList(unit), 
callback);
-      try {
-        return loader.loadClass(unit.getQualifiedClassName());
-      } catch (ClassNotFoundException e) {
-        throw new IllegalStateException("Impossible because we just compiled 
class", e);
-      }
-    }
-    checkRuntimeCodegenSupported();
-    DefineState defineState = getDefineState(unit.getQualifiedClassName());
-    if (defineState.definedClass != null) {
-      return defineState.definedClass;
-    }
-    synchronized (defineState.lock) {
-      if (defineState.definedClass != null) {
-        return defineState.definedClass;
-      }
-      ClassLoader parentClassLoader = getClassLoader();
-      Map<String, byte[]> classes =
-          compileToBytecode(java.util.Collections.singletonList(unit), 
parentClassLoader, callback);
-      byte[] bytecodes = classes.get(classFilepath(unit));
-      if (bytecodes == null) {
-        throw new IllegalStateException(
-            "Compiler did not produce bytecode for " + 
unit.getQualifiedClassName());
-      }
-      Class<?> definedClass = DefineClass.defineHiddenNestmate(neighborClass, 
bytecodes);
-      defineState.definedClass = definedClass;
-      defineState.defined = true;
-      return definedClass;
-    }
-  }
-
-  private Map<String, byte[]> compileToBytecode(
-      List<CompileUnit> compileUnits, ClassLoader parentClassLoader, 
CompileCallback callback) {
     CompileState compileState = getCompileState(compileUnits);
     callback.lock(compileState);
     Map<String, byte[]> classes;
@@ -192,7 +151,7 @@ public class CodeGenerator {
         compileState.lock.unlock();
       }
     }
-    return classes;
+    return defineClasses(classes);
   }
 
   /**
@@ -342,7 +301,6 @@ public class CodeGenerator {
   private static class DefineState {
     final Object lock;
     volatile boolean defined;
-    volatile Class<?> definedClass;
 
     private DefineState() {
       this.lock = new Object();
@@ -449,7 +407,7 @@ public class CodeGenerator {
   }
 
   public static String fullClassName(CompileUnit unit) {
-    return unit.getQualifiedClassName();
+    return unit.pkg + "." + unit.mainClassName;
   }
 
   public static String fullClassNameFromClassFilePath(String classFilePath) {
@@ -564,40 +522,10 @@ public class CodeGenerator {
     if (clz.isPrimitive()) {
       return true;
     }
-    if (!sourcePkgLevelAccessible(clz)) {
+    if (!ReflectionUtils.isPublic(clz)) {
       return false;
     }
-    Class<?> current = clz;
-    while (current != null) {
-      if (!ReflectionUtils.isPublic(current)) {
-        return false;
-      }
-      current = current.getEnclosingClass();
-    }
-    return true;
-  }
-
-  public static boolean sourceAccessibleFrom(Class<?> clz, String pkg) {
-    if (sourcePublicAccessible(clz)) {
-      return true;
-    }
-    if (clz.isArray()) {
-      return sourceAccessibleFrom(clz.getComponentType(), pkg);
-    }
-    if (!sourcePkgLevelAccessible(clz)) {
-      return false;
-    }
-    if (!ReflectionUtils.getPackage(clz).equals(pkg)) {
-      return false;
-    }
-    Class<?> current = clz;
-    while (current != null) {
-      if (ReflectionUtils.isPrivate(current)) {
-        return false;
-      }
-      current = current.getEnclosingClass();
-    }
-    return true;
+    return sourcePkgLevelAccessible(clz);
   }
 
   private static final Map<Class<?>, Boolean> sourcePkgLevelAccessible =
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java 
b/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java
index 779d7d9cf..ccfed3237 100644
--- a/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java
+++ b/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java
@@ -140,7 +140,6 @@ public class CodegenContext {
   String pkg;
   LinkedHashSet<String> imports = new LinkedHashSet<>();
   String className;
-  private boolean samePackageAccess;
   String classModifiers = "public final";
   String[] superClasses;
   String[] interfaces;
@@ -356,17 +355,6 @@ public class CodegenContext {
     return pkg;
   }
 
-  public void setSamePackageAccess(boolean samePackageAccess) {
-    if (this.samePackageAccess != samePackageAccess) {
-      sourcePublicAccessibleCache.clear();
-    }
-    this.samePackageAccess = samePackageAccess;
-  }
-
-  public boolean hasSamePackageAccess() {
-    return samePackageAccess;
-  }
-
   public Set<String> getValNames() {
     return valNames;
   }
@@ -741,12 +729,7 @@ public class CodegenContext {
 
   /** Returns true if class is public accessible from source. */
   public boolean sourcePublicAccessible(Class<?> clz) {
-    return sourcePublicAccessibleCache.computeIfAbsent(
-        clz,
-        c ->
-            samePackageAccess
-                ? CodeGenerator.sourceAccessibleFrom(c, pkg)
-                : CodeGenerator.sourcePublicAccessible(c));
+    return sourcePublicAccessibleCache.computeIfAbsent(clz, 
CodeGenerator::sourcePublicAccessible);
   }
 
   /** Returns true if class is package level accessible from source. */
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java 
b/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java
index d90b6fdae..f442c71c5 100644
--- a/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java
+++ b/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java
@@ -31,33 +31,19 @@ public class CompileUnit {
 
   String pkg;
   String mainClassName;
-  // Non-null only when the compiled class must be defined as a JDK25+ hidden 
nestmate of this
-  // neighbor. Ordinary generated classes still use the CodeGenerator 
classloader path.
-  private final Class<?> neighborClass;
   private String code;
   private Supplier<String> genCodeFunc;
 
   public CompileUnit(String pkg, String mainClassName, String code) {
-    this(pkg, mainClassName, code, null);
-  }
-
-  public CompileUnit(String pkg, String mainClassName, String code, Class<?> 
neighborClass) {
     this.pkg = pkg;
     this.mainClassName = mainClassName;
     this.code = code;
-    this.neighborClass = neighborClass;
   }
 
   public CompileUnit(String pkg, String mainClassName, Supplier<String> 
genCodeFunc) {
-    this(pkg, mainClassName, genCodeFunc, null);
-  }
-
-  public CompileUnit(
-      String pkg, String mainClassName, Supplier<String> genCodeFunc, Class<?> 
neighborClass) {
     this.pkg = pkg;
     this.mainClassName = mainClassName;
     this.genCodeFunc = genCodeFunc;
-    this.neighborClass = neighborClass;
   }
 
   public String getCode() {
@@ -79,10 +65,6 @@ public class CompileUnit {
     }
   }
 
-  public Class<?> getNeighborClass() {
-    return neighborClass;
-  }
-
   @Override
   public String toString() {
     return "CompileUnit{" + "pkg='" + pkg + '\'' + ", mainClassName='" + 
mainClassName + '\'' + '}';
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java 
b/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java
index 68eae904b..a9c3568e6 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java
@@ -68,12 +68,13 @@ public class ExpressionOptimizer {
       Expression groupExpressions,
       String methodPrefix,
       boolean inlineInvoke) {
-    // Janino lowers private instance helpers to static bridge methods with 
the original binary
-    // class as receiver. Hidden classes have a different runtime identity, so 
JDK25 hidden
-    // generated serializers must keep split helpers non-private.
-    String modifier = ctx.hasSamePackageAccess() ? "final" : "private";
     return invokeGenerated(
-        ctx, new LinkedHashSet<>(cutPoint), groupExpressions, modifier, 
methodPrefix, inlineInvoke);
+        ctx,
+        new LinkedHashSet<>(cutPoint),
+        groupExpressions,
+        "private",
+        methodPrefix,
+        inlineInvoke);
   }
 
   /**
@@ -132,7 +133,6 @@ public class ExpressionOptimizer {
     // instance field name.
     CodegenContext codegenContext =
         new CodegenContext(ctx.getPackage(), ctx.getValNames(), 
ctx.getImports());
-    codegenContext.setSamePackageAccess(ctx.hasSamePackageAccess());
     for (Reference reference : cutExprMap.values()) {
       
Preconditions.checkArgument(codegenContext.containName(reference.name()));
     }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java 
b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java
index 6dca2e503..b1000161b 100644
--- a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java
+++ b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java
@@ -55,9 +55,11 @@ public final class ReadContext {
   private final Generics generics;
   private final TypeResolver typeResolver;
   private final RefReader refReader;
+  private final TypeInfoHolder rootTypeInfoHolder;
   private final MetaStringReader metaStringReader;
   private final StringSerializer stringSerializer;
   private final boolean crossLanguage;
+  private final boolean trackingRef;
   private final boolean compressInt;
   private final Int64Encoding longEncoding;
   private final int maxDepth;
@@ -86,9 +88,11 @@ public final class ReadContext {
     this.generics = generics;
     this.typeResolver = typeResolver;
     this.refReader = refReader;
+    rootTypeInfoHolder = typeResolver.nilTypeInfoHolder();
     this.metaStringReader = metaStringReader;
     stringSerializer = (StringSerializer) 
typeResolver.getSerializer(String.class);
     crossLanguage = config.isXlang();
+    trackingRef = config.trackingRef();
     compressInt = config.compressInt();
     longEncoding = config.longEncoding();
     maxDepth = config.maxDepth();
@@ -578,6 +582,20 @@ public final class ReadContext {
     return (T) readNonRef(serializer);
   }
 
+  /** Reads the root object for one deserialization operation. */
+  public Object readRootRef() {
+    if (trackingRef) {
+      return readRef(rootTypeInfoHolder);
+    }
+    MemoryBuffer buffer = this.buffer;
+    int headFlag = buffer.readByte();
+    if (headFlag >= Fory.NOT_NULL_VALUE_FLAG) {
+      TypeInfo typeInfo = typeResolver.readTypeInfo(this, rootTypeInfoHolder);
+      return readNonRef(typeInfo);
+    }
+    return null;
+  }
+
   /** Reads a non-null, first-seen object together with its type metadata. */
   public Object readNonRef() {
     TypeInfo typeInfo = typeResolver.readTypeInfo(this);
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java 
b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java
index ebab5330b..bf74c0a67 100644
--- a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java
+++ b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java
@@ -56,9 +56,11 @@ public final class WriteContext {
   private final Generics generics;
   private final TypeResolver typeResolver;
   private final RefWriter refWriter;
+  private final TypeInfoHolder rootTypeInfoHolder;
   private final MetaStringWriter metaStringWriter;
   private final StringSerializer stringSerializer;
   private final boolean crossLanguage;
+  private final boolean trackingRef;
   private final boolean compressInt;
   private final Int64Encoding longEncoding;
   private final boolean forVirtualThread;
@@ -85,9 +87,11 @@ public final class WriteContext {
     this.generics = generics;
     this.typeResolver = typeResolver;
     this.refWriter = refWriter;
+    rootTypeInfoHolder = typeResolver.nilTypeInfoHolder();
     this.metaStringWriter = metaStringWriter;
     stringSerializer = (StringSerializer) 
typeResolver.getSerializer(String.class);
     crossLanguage = config.isXlang();
+    trackingRef = config.trackingRef();
     compressInt = config.compressInt();
     longEncoding = config.longEncoding();
     forVirtualThread = config.forVirtualThread();
@@ -527,6 +531,30 @@ public final class WriteContext {
     }
   }
 
+  /** Writes the root object for one serialization operation. */
+  public void writeRootRef(Object obj) {
+    if (trackingRef) {
+      writeRef(obj, rootTypeInfoHolder);
+      return;
+    }
+    MemoryBuffer buffer = this.buffer;
+    if (obj == null) {
+      buffer.writeByte(Fory.NULL_FLAG);
+      return;
+    }
+    buffer.writeByte(Fory.NOT_NULL_VALUE_FLAG);
+    TypeResolver resolver = typeResolver;
+    TypeInfo typeInfo = resolver.getTypeInfo(obj.getClass(), 
rootTypeInfoHolder);
+    if (crossLanguage && typeInfo.getType() == UnknownStruct.class) {
+      depth++;
+      typeInfo.getSerializer().write(this, obj);
+      depth--;
+      return;
+    }
+    resolver.writeTypeInfo(this, typeInfo);
+    writeData(typeInfo, obj);
+  }
+
   /**
    * Writes a non-null, first-seen object together with its type metadata.
    *
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java 
b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java
index 94dd96188..c0f002ca9 100644
--- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java
+++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java
@@ -962,7 +962,9 @@ public final class MemoryBuffer {
     } else {
       final int writerIdx = writerIndex;
       final int newIdx = writerIdx + 1;
-      ensure(newIdx);
+      if (newIdx > size) {
+        globalAllocator.grow(this, newIdx);
+      }
       final long pos = address + writerIdx;
       UNSAFE.putByte(heapMemory, pos, value);
       writerIndex = newIdx;
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
index 253c14b50..b0c8caf7c 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
@@ -42,6 +42,7 @@ import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Collection;
 import java.util.Collections;
@@ -245,7 +246,7 @@ public class ClassResolver extends TypeResolver {
   public static final short REPLACE_STUB_ID = INTERNAL_TYPE_START_ID + 28;
   public static final int NONEXISTENT_META_SHARED_ID = REPLACE_STUB_ID + 1;
 
-  private TypeInfo typeInfoCache;
+  private final TypeInfo[] typeInfoCache;
   // Every deserialization for unregistered class will query it, performance 
is important.
   private final ObjectMap<TypeNameBytes, TypeInfo> compositeNameBytes2TypeInfo 
=
       new ObjectMap<>(16, foryMapLoadFactor);
@@ -257,12 +258,17 @@ public class ClassResolver extends TypeResolver {
       SharedRegistry sharedRegistry,
       JITContext jitContext) {
     super(config, classLoader, sharedRegistry, jitContext);
-    typeInfoCache = NIL_TYPE_INFO;
+    typeInfoCache = new TypeInfo[config.maxDepth() + 
TYPE_INFO_CACHE_DEPTH_SLACK];
+    clearTypeInfoCache();
     extRegistry.classIdGenerator = NONEXISTENT_META_SHARED_ID + 1;
     shimDispatcher = new ShimDispatcher(this);
     _addGraalvmClassRegistry(config.getConfigHash(), this);
   }
 
+  private void clearTypeInfoCache() {
+    Arrays.fill(typeInfoCache, NIL_TYPE_INFO);
+  }
+
   @Override
   public void initialize() {
     extRegistry.objectGenericType = buildGenericType(OBJECT_TYPE);
@@ -1245,9 +1251,7 @@ public class ClassResolver extends TypeResolver {
           TypeNameBytes typeNameBytes = new TypeNameBytes(typeInfo.namespace, 
typeInfo.typeName);
           compositeNameBytes2TypeInfo.put(typeNameBytes, typeInfo);
         }
-        if (typeInfoCache.type == type) {
-          typeInfoCache = NIL_TYPE_INFO;
-        }
+        clearTypeInfoCache();
       }
     }
     // in order to support customized serializer for abstract or interface.
@@ -1306,7 +1310,7 @@ public class ClassResolver extends TypeResolver {
       if (cls.getName().equals(className)) {
         LOG.info("Clear serializer for class {}.", className);
         entry.getValue().setSerializer(this, Serializers.newSerializer(this, 
cls, serializer));
-        typeInfoCache = NIL_TYPE_INFO;
+        clearTypeInfoCache();
         return;
       }
     }
@@ -1323,7 +1327,7 @@ public class ClassResolver extends TypeResolver {
       if (className.startsWith(classNamePrefix)) {
         LOG.info("Clear serializer for class {}.", className);
         entry.getValue().setSerializer(this, Serializers.newSerializer(this, 
cls, serializer));
-        typeInfoCache = NIL_TYPE_INFO;
+        clearTypeInfoCache();
       }
     }
   }
@@ -1570,9 +1574,7 @@ public class ClassResolver extends TypeResolver {
             @Override
             public void onSuccess(Class<? extends Serializer> result) {
               setSerializer(clz, Serializers.newSerializer(ClassResolver.this, 
clz, result));
-              if (typeInfoCache.type == clz) {
-                typeInfoCache = NIL_TYPE_INFO; // clear class info cache
-              }
+              clearTypeInfoCache();
               Preconditions.checkState(getSerializer(clz).getClass() == 
result);
             }
 
@@ -1707,30 +1709,18 @@ public class ClassResolver extends TypeResolver {
 
   @Internal
   public TypeInfo getOrUpdateTypeInfo(Class<?> cls) {
-    TypeInfo typeInfo = typeInfoCache;
+    return getOrUpdateTypeInfo(cls, 0);
+  }
+
+  private TypeInfo getOrUpdateTypeInfo(Class<?> cls, int depth) {
+    TypeInfo typeInfo = typeInfoCache[depth];
     if (typeInfo.type != cls) {
       typeInfo = classInfoMap.get(cls);
       if (typeInfo == null || typeInfo.serializer == null) {
         addSerializer(cls, createSerializer(cls));
         typeInfo = classInfoMap.get(cls);
       }
-      typeInfoCache = typeInfo;
-    }
-    return typeInfo;
-  }
-
-  private TypeInfo getOrUpdateTypeInfo(short classId) {
-    TypeInfo typeInfo = typeInfoCache;
-    TypeInfo internalInfo = classId < typeIdToTypeInfo.length ? 
typeIdToTypeInfo[classId] : null;
-    Preconditions.checkArgument(
-        internalInfo != null, "Internal class id %s is not registered", 
classId);
-    if (typeInfo != internalInfo) {
-      typeInfo = internalInfo;
-      if (typeInfo.serializer == null) {
-        addSerializer(typeInfo.type, createSerializer(typeInfo.type));
-        typeInfo = classInfoMap.get(typeInfo.type);
-      }
-      typeInfoCache = typeInfo;
+      typeInfoCache[depth] = typeInfo;
     }
     return typeInfo;
   }
@@ -1881,7 +1871,7 @@ public class ClassResolver extends TypeResolver {
           .putCompatibleDeserializerClass(
               cls, CodecUtils.loadOrGenStaticCompatibleCodecClass(this, cls, 
typeDef));
     }
-    typeInfoCache = NIL_TYPE_INFO;
+    clearTypeInfoCache();
     if (RecordUtils.isRecord(cls)) {
       RecordUtils.getRecordConstructor(cls);
       RecordUtils.getRecordComponents(cls);
@@ -1995,7 +1985,7 @@ public class ClassResolver extends TypeResolver {
     } else if (cls == Long.class) {
       buffer.writeVarUInt32Small7(Types.INT64);
     } else {
-      writeTypeInfo(writeContext, getOrUpdateTypeInfo(cls));
+      writeTypeInfo(writeContext, getOrUpdateTypeInfo(cls, 
writeContext.getDepth()));
     }
   }
 
@@ -2474,7 +2464,7 @@ public class ClassResolver extends TypeResolver {
             }
           });
       if (GraalvmSupport.isGraalBuildTime()) {
-        typeInfoCache = NIL_TYPE_INFO;
+        clearTypeInfoCache();
         clearGraalvmGeneratedTypeInfoSerializers();
         compositeNameBytes2TypeInfo.forEach(
             (typeNameBytes, typeInfo) -> 
clearGraalvmTypeInfoSerializer(typeInfo));
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
index b10498552..275ccf1d0 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
@@ -123,6 +123,7 @@ public abstract class TypeResolver {
   static final int INTERNAL_NATIVE_ID_LIMIT = 250;
   private static final GenericType OBJECT_GENERIC_TYPE = 
GenericType.build(Object.class);
   private static final float TYPE_ID_MAP_LOAD_FACTOR = 0.5f;
+  static final int TYPE_INFO_CACHE_DEPTH_SLACK = 10;
   static final long MAX_USER_TYPE_ID = 0xffff_fffEL;
 
   private static final class TransformedTypeInfo {
@@ -149,9 +150,9 @@ public abstract class TypeResolver {
   final TypeInfo[] typeIdToTypeInfo;
   // Map for user-registered type ids, keyed by user id.
   final LongMap<TypeInfo> userTypeIdToTypeInfo = new LongMap<>(4, 
TYPE_ID_MAP_LOAD_FACTOR);
-  // Cache for readTypeInfo(MemoryBuffer) - persists between calls to avoid 
reloading
+  // Caches for readTypeInfo(ReadContext) - persist between calls to avoid 
reloading
   // dynamically created classes that can't be found by Class.forName
-  private TypeInfo typeInfoCache;
+  private final TypeInfo[] typeInfoCache;
   private boolean registrationFinished;
 
   protected TypeResolver(
@@ -167,6 +168,8 @@ public abstract class TypeResolver {
     typeDefMap = sharedRegistry.typeDefMap;
     int length = isCrossLanguage() ? Types.BOUND : INTERNAL_NATIVE_ID_LIMIT;
     typeIdToTypeInfo = new TypeInfo[length];
+    typeInfoCache = new TypeInfo[config.maxDepth() + 
TYPE_INFO_CACHE_DEPTH_SLACK];
+    Arrays.fill(typeInfoCache, NIL_TYPE_INFO);
   }
 
   public final Config getConfig() {
@@ -557,6 +560,27 @@ public abstract class TypeResolver {
     }
   }
 
+  // Generated exact-class branches call this only after code generation 
resolves the TypeInfo to a
+  // compatible struct id. The wire body stays in writeSharedClassMeta; this 
helper only skips the
+  // generic type-id switch.
+  @Internal
+  public final void writeCompatibleTypeInfo(
+      WriteContext writeContext, MemoryBuffer buffer, TypeInfo typeInfo) {
+    assert isCompatibleStructTypeId(typeInfo.typeId);
+    buffer.writeUInt8(typeInfo.typeId);
+    writeSharedClassMeta(writeContext, buffer, typeInfo);
+  }
+
+  private static boolean isCompatibleStructTypeId(int typeId) {
+    switch (typeId) {
+      case Types.COMPATIBLE_STRUCT:
+      case Types.NAMED_COMPATIBLE_STRUCT:
+        return true;
+      default:
+        return false;
+    }
+  }
+
   static int toUserTypeId(long userTypeId) {
     Preconditions.checkArgument(
         userTypeId >= 0 && userTypeId <= MAX_USER_TYPE_ID,
@@ -576,6 +600,21 @@ public abstract class TypeResolver {
     return new Invoke(classResolverRef, "writeTypeInfo", buffer, classInfo);
   }
 
+  // Note: Thread safe for jit thread to call.
+  public Expression writeExactClassExpr(
+      Expression classResolverRef,
+      Expression writeContext,
+      Expression buffer,
+      Expression classInfo,
+      Class<?> cls) {
+    TypeInfo typeInfo = getTypeInfo(cls);
+    if (isCompatibleStructTypeId(typeInfo.typeId)) {
+      return new Invoke(
+          classResolverRef, "writeCompatibleTypeInfo", writeContext, buffer, 
classInfo);
+    }
+    return writeClassExpr(classResolverRef, writeContext, classInfo);
+  }
+
   /**
    * Writes shared class metadata using the meta-share protocol. Protocol: If 
class already written,
    * writes {@code (index << 1) | 1} (reference). If new class, writes {@code 
(index << 1)} followed
@@ -585,6 +624,11 @@ public abstract class TypeResolver {
    */
   protected final void writeSharedClassMeta(WriteContext writeContext, 
TypeInfo typeInfo) {
     MemoryBuffer buffer = writeContext.getBuffer();
+    writeSharedClassMeta(writeContext, buffer, typeInfo);
+  }
+
+  private void writeSharedClassMeta(
+      WriteContext writeContext, MemoryBuffer buffer, TypeInfo typeInfo) {
     MetaWriteContext metaWriteContext = writeContext.getMetaWriteContext();
     assert metaWriteContext != null : SET_META_WRITE_CONTEXT_MSG;
     IdentityObjectIntMap<Class<?>> classMap = metaWriteContext.classMap;
@@ -619,6 +663,8 @@ public abstract class TypeResolver {
    */
   public final TypeInfo readTypeInfo(ReadContext readContext) {
     MemoryBuffer buffer = readContext.getBuffer();
+    int depth = readContext.getDepth();
+    TypeInfo cachedTypeInfo = typeInfoCache[depth];
     int typeId = buffer.readUInt8();
     TypeInfo typeInfo;
     switch (typeId) {
@@ -626,20 +672,20 @@ public abstract class TypeResolver {
       case Types.STRUCT:
       case Types.EXT:
       case Types.TYPED_UNION:
-        typeInfo = 
Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32()));
+        typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), 
cachedTypeInfo);
         break;
       case Types.COMPATIBLE_STRUCT:
       case Types.NAMED_COMPATIBLE_STRUCT:
-        typeInfo = readSharedClassTypeInfo(readContext, null);
+        typeInfo = readSharedClassTypeInfo(readContext, null, cachedTypeInfo);
         break;
       case Types.NAMED_ENUM:
       case Types.NAMED_STRUCT:
       case Types.NAMED_EXT:
       case Types.NAMED_UNION:
         if (!metaContextShareEnabled) {
-          typeInfo = readTypeInfoFromBytes(readContext, typeInfoCache, typeId);
+          typeInfo = readTypeInfoFromBytes(readContext, cachedTypeInfo, 
typeId);
         } else {
-          typeInfo = readSharedClassTypeInfo(readContext, null);
+          typeInfo = readSharedClassTypeInfo(readContext, null, 
cachedTypeInfo);
         }
         break;
       case Types.LIST:
@@ -654,7 +700,7 @@ public abstract class TypeResolver {
     if (typeInfo.serializer == null) {
       typeInfo = ensureSerializerForTypeInfo(typeInfo);
     }
-    typeInfoCache = typeInfo;
+    typeInfoCache[depth] = typeInfo;
     return typeInfo;
   }
 
@@ -664,6 +710,8 @@ public abstract class TypeResolver {
    */
   public final TypeInfo readTypeInfo(ReadContext readContext, Class<?> 
targetClass) {
     MemoryBuffer buffer = readContext.getBuffer();
+    int depth = readContext.getDepth();
+    TypeInfo cachedTypeInfo = typeInfoCache[depth];
     int typeId = buffer.readUInt8();
     TypeInfo typeInfo;
     switch (typeId) {
@@ -671,20 +719,20 @@ public abstract class TypeResolver {
       case Types.STRUCT:
       case Types.EXT:
       case Types.TYPED_UNION:
-        typeInfo = 
Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32()));
+        typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), 
cachedTypeInfo);
         break;
       case Types.COMPATIBLE_STRUCT:
       case Types.NAMED_COMPATIBLE_STRUCT:
-        typeInfo = readSharedClassMeta(readContext, targetClass);
+        typeInfo = readSharedClassMeta(readContext, targetClass, 
cachedTypeInfo);
         break;
       case Types.NAMED_ENUM:
       case Types.NAMED_STRUCT:
       case Types.NAMED_EXT:
       case Types.NAMED_UNION:
         if (!metaContextShareEnabled) {
-          typeInfo = readTypeInfoFromBytes(readContext, typeInfoCache, typeId);
+          typeInfo = readTypeInfoFromBytes(readContext, cachedTypeInfo, 
typeId);
         } else {
-          typeInfo = readSharedClassMeta(readContext, targetClass);
+          typeInfo = readSharedClassMeta(readContext, targetClass, 
cachedTypeInfo);
         }
         break;
       case Types.LIST:
@@ -699,7 +747,7 @@ public abstract class TypeResolver {
     if (typeInfo.serializer == null) {
       typeInfo = ensureSerializerForTypeInfo(typeInfo);
     }
-    typeInfoCache = typeInfo;
+    typeInfoCache[depth] = typeInfo;
     return typeInfo;
   }
 
@@ -721,7 +769,7 @@ public abstract class TypeResolver {
       case Types.STRUCT:
       case Types.EXT:
       case Types.TYPED_UNION:
-        typeInfo = 
Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32()));
+        typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), 
typeInfoCache);
         break;
       case Types.COMPATIBLE_STRUCT:
       case Types.NAMED_COMPATIBLE_STRUCT:
@@ -763,28 +811,27 @@ public abstract class TypeResolver {
   public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder 
classInfoHolder) {
     MemoryBuffer buffer = readContext.getBuffer();
     int typeId = buffer.readUInt8();
+    TypeInfo cachedTypeInfo = classInfoHolder.typeInfo;
     TypeInfo typeInfo;
-    boolean updateCache = false;
     switch (typeId) {
       case Types.ENUM:
       case Types.STRUCT:
       case Types.EXT:
       case Types.TYPED_UNION:
-        typeInfo = 
Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32()));
+        typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), 
cachedTypeInfo);
         break;
       case Types.COMPATIBLE_STRUCT:
       case Types.NAMED_COMPATIBLE_STRUCT:
-        typeInfo = readSharedClassTypeInfo(readContext, null);
+        typeInfo = readSharedClassTypeInfo(readContext, null, cachedTypeInfo);
         break;
       case Types.NAMED_ENUM:
       case Types.NAMED_STRUCT:
       case Types.NAMED_EXT:
       case Types.NAMED_UNION:
         if (!metaContextShareEnabled) {
-          typeInfo = readTypeInfoFromBytes(readContext, 
classInfoHolder.typeInfo, typeId);
-          updateCache = true;
+          typeInfo = readTypeInfoFromBytes(readContext, cachedTypeInfo, 
typeId);
         } else {
-          typeInfo = readSharedClassTypeInfo(readContext, null);
+          typeInfo = readSharedClassTypeInfo(readContext, null, 
cachedTypeInfo);
         }
         break;
       case Types.LIST:
@@ -796,22 +843,24 @@ public abstract class TypeResolver {
       default:
         typeInfo = Objects.requireNonNull(getInternalTypeInfoByTypeId(typeId));
     }
+    // Do not return internal TypeInfo above before this check: lazy codegen 
can briefly clear a
+    // cached TypeInfo serializer while resolving recursive serializers.
     if (typeInfo.serializer == null) {
       typeInfo = ensureSerializerForTypeInfo(typeInfo);
     }
-    if (updateCache) {
+    if (typeInfo != cachedTypeInfo) {
       classInfoHolder.typeInfo = typeInfo;
     }
     return typeInfo;
   }
 
-  /**
-   * Read class info using the provided cache. Returns cached TypeInfo if the 
namespace and type
-   * name bytes match.
-   */
-  protected final TypeInfo readTypeInfoByCache(
-      ReadContext readContext, TypeInfo typeInfoCache, int header) {
-    return readTypeInfoFromBytes(readContext, typeInfoCache, header);
+  private TypeInfo readRegisteredTypeInfo(int typeId, int userTypeId, TypeInfo 
cachedTypeInfo) {
+    TypeInfo typeInfo = cachedTypeInfo;
+    if (typeInfo.typeId != typeId || typeInfo.userTypeId != userTypeId) {
+      typeInfo = userTypeIdToTypeInfo.get(userTypeId);
+      assert typeInfo != null;
+    }
+    return typeInfo;
   }
 
   /**
@@ -849,7 +898,12 @@ public abstract class TypeResolver {
   }
 
   public final TypeInfo readSharedClassMeta(ReadContext readContext, Class<?> 
targetClass) {
-    TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass);
+    return readSharedClassMeta(readContext, targetClass, null);
+  }
+
+  private TypeInfo readSharedClassMeta(
+      ReadContext readContext, Class<?> targetClass, TypeInfo cachedTypeInfo) {
+    TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass, 
cachedTypeInfo);
     Class<?> readClass = typeInfo.getType();
     if (targetClass != readClass) {
       return getTargetTypeInfo(typeInfo, targetClass);
@@ -858,6 +912,11 @@ public abstract class TypeResolver {
   }
 
   private TypeInfo readSharedClassTypeInfo(ReadContext readContext, Class<?> 
targetClass) {
+    return readSharedClassTypeInfo(readContext, targetClass, null);
+  }
+
+  private TypeInfo readSharedClassTypeInfo(
+      ReadContext readContext, Class<?> targetClass, TypeInfo cachedTypeInfo) {
     MemoryBuffer buffer = readContext.getBuffer();
     MetaReadContext metaReadContext = readContext.getMetaReadContext();
     assert metaReadContext != null : SET_META_READ_CONTEXT_MSG;
@@ -875,7 +934,14 @@ public abstract class TypeResolver {
       // body/hash/schema-limit/exact-local checks here; the header-miss path 
owns them before
       // cache publish.
       long id = buffer.readInt64();
-      typeInfo = extRegistry.typeInfoByTypeDefId.get(id);
+      TypeDef cachedTypeDef = cachedTypeInfo == null ? null : 
cachedTypeInfo.getTypeDef();
+      // A field-local cache hit is valid only when the cached TypeInfo 
carries the exact checked
+      // TypeDef id that was parsed and accepted earlier by this resolver.
+      if (cachedTypeDef != null && cachedTypeDef.getId() == id) {
+        typeInfo = cachedTypeInfo;
+      } else {
+        typeInfo = extRegistry.typeInfoByTypeDefId.get(id);
+      }
       if (typeInfo != null) {
         TypeDef.skipTypeDef(buffer, id);
       } else {
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java 
b/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java
index f07098d6c..7e7e1d27b 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java
@@ -26,11 +26,13 @@ import java.lang.reflect.Modifier;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
+import org.apache.fory.annotation.CodegenInvoke;
 import org.apache.fory.annotation.ForyEnumId;
 import org.apache.fory.collection.LongMap;
 import org.apache.fory.config.Config;
 import org.apache.fory.context.ReadContext;
 import org.apache.fory.context.WriteContext;
+import org.apache.fory.memory.MemoryBuffer;
 import org.apache.fory.util.Preconditions;
 
 @SuppressWarnings("rawtypes")
@@ -65,15 +67,25 @@ public class EnumSerializer extends 
ImmutableSerializer<Enum> implements Shareab
 
   @Override
   public void write(WriteContext writeContext, Enum value) {
+    writeValue(writeContext, writeContext.getBuffer(), value);
+  }
+
+  @CodegenInvoke
+  public final void writeValue(WriteContext writeContext, MemoryBuffer buffer, 
Enum value) {
     if (!config.isXlang() && config.serializeEnumByName()) {
       writeContext.writeString(value.name());
     } else {
-      
writeContext.getBuffer().writeVarUInt32Small7(tagByOrdinal[value.ordinal()]);
+      buffer.writeVarUInt32Small7(tagByOrdinal[value.ordinal()]);
     }
   }
 
   @Override
   public Enum read(ReadContext readContext) {
+    return readValue(readContext, readContext.getBuffer());
+  }
+
+  @CodegenInvoke
+  public final Enum readValue(ReadContext readContext, MemoryBuffer buffer) {
     if (!config.isXlang() && config.serializeEnumByName()) {
       String name = readContext.readString();
       Enum e = stringToEnum.get(name);
@@ -82,7 +94,7 @@ public class EnumSerializer extends ImmutableSerializer<Enum> 
implements Shareab
       }
       return handleUnknownEnumValue(name);
     } else {
-      int tag = readContext.getBuffer().readVarUInt32Small7();
+      int tag = buffer.readVarUInt32Small7();
       Enum value = null;
       if (enumConstantByTagArray != null && tag < 
enumConstantByTagArray.length) {
         value = enumConstantByTagArray[tag];
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java 
b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java
index bb3d93f22..2ad935101 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java
@@ -537,16 +537,19 @@ public final class StringSerializer extends 
ImmutableSerializer<String> {
     int bytesLen = bytes.length;
     long header = ((long) bytesLen << 2) | coder;
     int writerIndex = buffer.writerIndex();
-    // The `ensure` ensure next operations are safe without bound checks,
-    // and inner heap buffer doesn't change.
-    buffer.ensure(writerIndex + 9 + bytesLen); // 1 byte coder + varint max 8 
bytes
+    buffer.ensure(writerIndex + 9 + bytesLen);
     final byte[] targetArray = buffer.getHeapMemory();
     if (targetArray != null) {
-      // Some JDK11 Unsafe.copyMemory will `copyMemoryChecks`, and
-      // jvm doesn't eliminate well in some jdk.
       final int targetIndex = buffer._unsafeHeapWriterIndex();
       int arrIndex = targetIndex;
-      arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, 
header);
+      if (header >>> 7 == 0) {
+        targetArray[arrIndex++] = (byte) header;
+      } else if (header >>> 14 == 0) {
+        targetArray[arrIndex++] = (byte) ((header & 0x7F) | 0x80);
+        targetArray[arrIndex++] = (byte) (header >>> 7);
+      } else {
+        arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, 
header);
+      }
       writerIndex += arrIndex - targetIndex;
       System.arraycopy(bytes, 0, targetArray, arrIndex, bytesLen);
     } else {
@@ -644,7 +647,7 @@ public final class StringSerializer extends 
ImmutableSerializer<String> {
     byte[] heapMemory = buffer.getHeapMemory();
     if (heapMemory != null) {
       final int arrIndex = buffer._unsafeHeapReaderIndex();
-      buffer.increaseReaderIndex(numBytes);
+      buffer._increaseReaderIndexUnsafe(numBytes);
       bytes = new byte[numBytes];
       System.arraycopy(heapMemory, arrIndex, bytes, 0, numBytes);
     } else {
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java 
b/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java
index 15b53b575..aadd27071 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java
@@ -175,12 +175,13 @@ public class UnionSerializer extends Serializer<Union> {
     if (nextReadRefId >= Fory.NOT_NULL_VALUE_FLAG) {
       // ref value or not-null value
       TypeInfo declared = getFinalCaseTypeInfo(caseId);
-      TypeInfo readTypeInfo = resolver.readTypeInfo(readContext, declared);
       if (declared != null) {
+        TypeInfo readTypeInfo = resolver.readTypeInfo(readContext, declared);
         Serializer serializer = getCaseSerializer(caseId, 
readTypeInfo.getTypeId(), declared);
         GenericType genericType = getCaseGenericType(caseId, 
readTypeInfo.getTypeId());
         caseValue = readCaseValue(readContext, serializer, genericType);
       } else {
+        TypeInfo readTypeInfo = resolver.readTypeInfo(readContext);
         caseValue = Serializers.read(readContext, 
readTypeInfo.getSerializer());
       }
       readContext.setReadRef(nextReadRefId, caseValue);
@@ -333,7 +334,12 @@ public class UnionSerializer extends Serializer<Union> {
     int nextReadRefId = readContext.tryPreserveRefId();
     if (nextReadRefId >= Fory.NOT_NULL_VALUE_FLAG) {
       TypeInfo declared = getDeclaredCaseTypeInfo(fieldInfo, typeId);
-      TypeInfo readTypeInfo = resolver.readTypeInfo(readContext, declared);
+      TypeInfo readTypeInfo;
+      if (declared != null) {
+        readTypeInfo = resolver.readTypeInfo(readContext, declared);
+      } else {
+        readTypeInfo = resolver.readTypeInfo(readContext);
+      }
       Serializer serializer = getCaseSerializer(fieldInfo, 
readTypeInfo.getTypeId(), readTypeInfo);
       Object caseValue =
           readCaseValue(
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java
index ef03486e1..1356d80a3 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java
@@ -21,6 +21,7 @@ package org.apache.fory.serializer;
 
 import java.util.ArrayList;
 import java.util.List;
+import org.apache.fory.annotation.CodegenInvoke;
 import org.apache.fory.collection.IdentityObjectIntMap;
 import org.apache.fory.collection.LongMap;
 import org.apache.fory.collection.MapEntry;
@@ -294,21 +295,32 @@ public final class UnknownClassSerializers {
 
     @Override
     public void write(WriteContext writeContext, UnknownEnum value) {
+      writeValue(writeContext, writeContext.getBuffer(), value);
+    }
+
+    @CodegenInvoke
+    public final void writeValue(
+        WriteContext writeContext, MemoryBuffer buffer, UnknownEnum value) {
       if (!config.isXlang() && config.serializeEnumByName()) {
         writeContext.writeString(value.name());
       } else {
-        writeContext.getBuffer().writeVarUInt32Small7(value.ordinal());
+        buffer.writeVarUInt32Small7(value.ordinal());
       }
     }
 
     @Override
     public UnknownEnum read(ReadContext readContext) {
+      return readValue(readContext, readContext.getBuffer());
+    }
+
+    @CodegenInvoke
+    public final UnknownEnum readValue(ReadContext readContext, MemoryBuffer 
buffer) {
       if (!config.isXlang() && config.serializeEnumByName()) {
         readContext.readString();
         return UnknownEnum.UNKNOWN;
       }
 
-      int ordinal = readContext.getBuffer().readVarUInt32Small7();
+      int ordinal = buffer.readVarUInt32Small7();
       if (ordinal >= enumConstants.length) {
         return UnknownEnum.UNKNOWN;
       }
diff --git 
a/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java 
b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java
index 7fbf021ff..52c4d0ce5 100644
--- a/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java
+++ b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java
@@ -1108,7 +1108,9 @@ public final class MemoryBuffer {
   public void writeByte(byte value) {
     final int writerIdx = writerIndex;
     final int newIdx = writerIdx + 1;
-    ensure(newIdx);
+    if (newIdx > size) {
+      globalAllocator.grow(this, newIdx);
+    }
     final long pos = address + writerIdx;
     storeByte(pos, value);
     writerIndex = newIdx;
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java 
b/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java
index 41702a442..c347b628a 100644
--- a/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java
@@ -20,14 +20,11 @@
 package org.apache.fory.builder;
 
 import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertSame;
-import static org.testng.Assert.assertTrue;
 
 import org.apache.fory.Fory;
 import org.apache.fory.ForyTestBase;
 import org.apache.fory.memory.MemoryBuffer;
 import org.apache.fory.memory.MemoryUtils;
-import org.apache.fory.platform.JdkVersion;
 import org.apache.fory.resolver.TypeResolver;
 import org.apache.fory.test.bean.BeanA;
 import org.testng.annotations.Test;
@@ -45,7 +42,6 @@ public class CodecUtilsTest {
             .withCompatible(false)
             .build();
     Class<?> seqCodecClass = 
fory.getTypeResolver().getSerializerClass(BeanA.class);
-    assertGeneratedClassShape(seqCodecClass, BeanA.class);
     Generated.GeneratedSerializer serializer =
         seqCodecClass
             .asSubclass(Generated.GeneratedSerializer.class)
@@ -58,15 +54,4 @@ public class CodecUtilsTest {
     Object obj = ForyTestBase.readSerializer(fory, serializer, 
MemoryUtils.wrap(bytes));
     assertEquals(obj, beanA);
   }
-
-  private static void assertGeneratedClassShape(Class<?> serializerClass, 
Class<?> beanClass)
-      throws Exception {
-    if (JdkVersion.MAJOR_VERSION >= 25) {
-      assertTrue((Boolean) 
Class.class.getMethod("isHidden").invoke(serializerClass));
-      assertSame(Class.class.getMethod("getNestHost").invoke(serializerClass), 
beanClass);
-    } else {
-      assertSame(
-          
serializerClass.getClassLoader().loadClass(serializerClass.getName()), 
serializerClass);
-    }
-  }
 }
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java
index 756c00e55..c91041495 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java
+++ 
b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java
@@ -60,6 +60,41 @@ public class StringSerializerTest extends ForyTestBase {
     Assert.assertThrows(IllegalArgumentException.class, () -> 
serializer.readString(buffer));
   }
 
+  @Test
+  public void testBytesStringWire() {
+    if (!stringValueIsBytes()) {
+      throw new SkipException("Skip when string value is not byte[]");
+    }
+    String[] values = {
+      "",
+      "a",
+      StringUtils.random(31),
+      StringUtils.random(40),
+      StringUtils.random(5000),
+      "你好",
+      "abc你好"
+    };
+    for (String value : values) {
+      int capacity = Math.max(64, value.length() * 8 + 64);
+      MemoryBuffer buffer = MemoryBuffer.newHeapBuffer(capacity);
+      StringSerializer.writeBytesString(buffer, value);
+      Assert.assertEquals(
+          readJDK11String(MemoryBuffer.fromByteArray(buffer.getBytes(0, 
buffer.writerIndex()))),
+          value);
+    }
+  }
+
+  private static boolean stringValueIsBytes() {
+    try {
+      Field valueIsBytesField =
+          
StringSerializer.class.getDeclaredField("STRING_VALUE_FIELD_IS_BYTES");
+      valueIsBytesField.setAccessible(true);
+      return (boolean) valueIsBytesField.get(null);
+    } catch (ReflectiveOperationException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   @Test
   public void testJavaStringZeroCopy() {
     if (JdkVersion.MAJOR_VERSION >= 17) {
diff --git a/java/fory-json/pom.xml b/java/fory-json/pom.xml
index bfaca0bda..5fd9652b7 100644
--- a/java/fory-json/pom.xml
+++ b/java/fory-json/pom.xml
@@ -39,6 +39,8 @@
     <maven.compiler.source>8</maven.compiler.source>
     <maven.compiler.target>8</maven.compiler.target>
     <fory.java.rootdir>${basedir}/..</fory.java.rootdir>
+    
<fory.json.jdk25.test.classes>${project.build.directory}/jdk25-test-classes</fory.json.jdk25.test.classes>
+    
<fory.core.jdk25.test.classes>${project.basedir}/../fory-core/target/jdk25-test-classes</fory.core.jdk25.test.classes>
   </properties>
 
   <dependencies>
@@ -153,5 +155,57 @@
         </plugins>
       </build>
     </profile>
+    <profile>
+      <id>jdk25-core-test-overlay</id>
+      <activation>
+        <jdk>[25,)</jdk>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-antrun-plugin</artifactId>
+            <version>3.1.0</version>
+            <executions>
+              <execution>
+                <id>prepare-jdk25-test-classes</id>
+                <phase>process-test-classes</phase>
+                <goals>
+                  <goal>run</goal>
+                </goals>
+                <configuration>
+                  <target>
+                    <delete dir="${fory.json.jdk25.test.classes}"/>
+                    <mkdir dir="${fory.json.jdk25.test.classes}"/>
+                    <!-- Reactor tests see dependency target/classes instead 
of the packaged
+                         multi-release jar. Put Fory core's JDK25 test overlay 
ahead of the raw
+                         dependency classes so JsonCodegen exercises the same 
CodecBuilder owner
+                         path as packaged JDK25 runs. -->
+                    <copy todir="${fory.json.jdk25.test.classes}">
+                      <fileset dir="${project.build.outputDirectory}">
+                        <exclude name="module-info.class"/>
+                      </fileset>
+                    </copy>
+                    <copy todir="${fory.json.jdk25.test.classes}" 
overwrite="true">
+                      <fileset dir="${fory.core.jdk25.test.classes}" 
erroronmissingdir="false">
+                        <exclude name="module-info.class"/>
+                      </fileset>
+                    </copy>
+                  </target>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-surefire-plugin</artifactId>
+            <configuration>
+              <argLine>${argLine} 
--add-opens=java.base/java.lang.invoke=ALL-UNNAMED</argLine>
+              
<classesDirectory>${fory.json.jdk25.test.classes}</classesDirectory>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
   </profiles>
 </project>
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java 
b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java
index a98c7e6d5..df786e028 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java
@@ -40,7 +40,6 @@ import org.apache.fory.json.reader.Utf8ObjectReader;
 import org.apache.fory.json.resolver.JsonTypeResolver;
 import org.apache.fory.json.writer.StringObjectWriter;
 import org.apache.fory.json.writer.Utf8ObjectWriter;
-import org.apache.fory.platform.JdkVersion;
 import org.apache.fory.util.record.RecordUtils;
 
 public final class JsonCodegen {
@@ -121,8 +120,7 @@ public final class JsonCodegen {
         new JsonGeneratedCodecBuilder(this, className, type, properties, utf8, 
true, false)
             .genCode();
     try {
-      CompileUnit unit = new CompileUnit(PACKAGE, className, code, 
JsonCodegen.class);
-      Class<?> writerClass = codeGenerator.compileAndLoad(unit, state -> 
state.lock.lock());
+      Class<?> writerClass = compileClass(className, code);
       Constructor<?> constructor =
           writerClass.getDeclaredConstructor(JsonFieldInfo[].class, 
JsonCodec[].class);
       constructor.setAccessible(true);
@@ -143,8 +141,7 @@ public final class JsonCodegen {
         new JsonGeneratedCodecBuilder(this, className, type, properties, 
false, false, record)
             .genCode();
     try {
-      CompileUnit unit = new CompileUnit(PACKAGE, className, code, 
JsonCodegen.class);
-      Class<?> readerClass = codeGenerator.compileAndLoad(unit, state -> 
state.lock.lock());
+      Class<?> readerClass = compileClass(className, code);
       Constructor<?> constructor =
           readerClass.getDeclaredConstructor(
               JsonFieldInfo[].class, JsonCodec[].class, 
BaseObjectCodec[].class);
@@ -155,6 +152,12 @@ public final class JsonCodegen {
     }
   }
 
+  private Class<?> compileClass(String className, String code) throws 
ClassNotFoundException {
+    CompileUnit unit = new CompileUnit(PACKAGE, className, code);
+    ClassLoader classLoader = codeGenerator.compile(unit);
+    return classLoader.loadClass(PACKAGE + "." + className);
+  }
+
   private static JsonCodec[] writeCodecs(JsonFieldInfo[] properties) {
     JsonCodec[] codecs = new JsonCodec[properties.length];
     for (int i = 0; i < properties.length; i++) {
@@ -230,15 +233,7 @@ public final class JsonCodegen {
   }
 
   private boolean canCompile(Class<?> type) {
-    return supportsHiddenNestmateLoading()
-        && CodeGenerator.sourcePublicAccessible(type)
-        && isVisible(type);
-  }
-
-  private static boolean supportsHiddenNestmateLoading() {
-    // Generated JSON codecs are defined as hidden nestmates of JsonCodegen; 
JDK 8 must keep using
-    // the interpreter path because hidden classes are unavailable there.
-    return JdkVersion.MAJOR_VERSION >= 15;
+    return CodeGenerator.sourcePublicAccessible(type) && isVisible(type);
   }
 
   private boolean isVisible(Class<?> type) {
diff --git 
a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java 
b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java
index cdbd4b4d1..390eca491 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java
@@ -50,7 +50,6 @@ import org.apache.fory.json.data.PrivateFields;
 import org.apache.fory.json.data.PublicFields;
 import org.apache.fory.json.data.TokenValues;
 import org.apache.fory.json.data.UnicodeMatrix;
-import org.apache.fory.platform.JdkVersion;
 import org.apache.fory.reflect.FieldAccessor;
 import org.testng.SkipException;
 
@@ -270,7 +269,7 @@ public abstract class ForyJsonTestModels {
   }
 
   protected static void assertGeneratedWhenSupported(ForyJson json, Class<?> 
type) {
-    assertEquals(json.hasGeneratedWriter(type), JdkVersion.MAJOR_VERSION >= 
15);
+    assertTrue(json.hasGeneratedWriter(type));
   }
 
   protected static String repeat(char ch, int length) {


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


Reply via email to