You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fory.apache.org by ch...@apache.org on 2026/08/03 05:11:50 UTC

(fory) branch main updated: feat(json): add GraalVM codegen support for json (#3907)

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 3194ad514 feat(json): add GraalVM codegen support for json (#3907)
3194ad514 is described below

commit 3194ad514dba41ec466818c71cb799408fd0244a
Author: Shawn Yang <sh...@gmail.com>
AuthorDate: Mon Aug 3 13:11:45 2026 +0800

    feat(json): add GraalVM codegen support for json (#3907)
    
    ## 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
---
 ci/run_ci.sh                                       |   8 +-
 docs/guide/java/graalvm-support.md                 | 125 +++---
 docs/guide/java/json-support.md                    |  36 +-
 integration_tests/graalvm_tests/README.md          |  14 +-
 integration_tests/graalvm_tests/pom.xml            |   8 +-
 .../graalvm_tests/src/main/java/module-info.java   |   3 +-
 .../org/apache/fory/graalvm/ForyJsonExample.java   | 421 ++++++++++++++++++-
 .../fory/graalvm/closed/ClosedJsonConfigs.java     |  68 +++
 java/README.md                                     |   7 +-
 .../processing/GeneratedJsonCodecSourceWriter.java |  13 +-
 .../annotation/processing/JsonTypeProcessor.java   |  28 +-
 .../processing/JsonTypeProcessorTest.java          |  17 -
 .../apache/fory/platform/internal/DefineClass.java |   9 +-
 .../apache/fory/platform/internal/_JDKAccess.java  |  17 +-
 .../fory-core/native-image.properties              |   3 +
 java/fory-json/README.md                           |  36 +-
 .../main/java/org/apache/fory/json/ForyJson.java   |   6 +
 .../java/org/apache/fory/json/ForyJsonBuilder.java |  10 +-
 .../main/java/org/apache/fory/json/JsonConfig.java |  69 +---
 .../fory/json/annotation/ForyJsonProvider.java     |  42 ++
 .../org/apache/fory/json/annotation/JsonType.java  |  11 +-
 .../apache/fory/json/codec/CollectionCodec.java    |  13 +
 .../fory/json/codec/GeneratedJsonCodecFactory.java |  28 --
 .../java/org/apache/fory/json/codec/MapCodec.java  |  13 +
 .../org/apache/fory/json/codec/ObjectCodec.java    |  42 +-
 .../apache/fory/json/codec/ObjectCodecBuilder.java |   7 +-
 .../org/apache/fory/json/codec/SqlJsonCodecs.java  |  30 +-
 .../org/apache/fory/json/codegen/JsonCodegen.java  |  96 ++++-
 .../apache/fory/json/codegen/JsonCodegenKey.java   |  71 ++++
 .../org/apache/fory/json/meta/JsonCreatorInfo.java | 101 +++--
 .../apache/fory/json/meta/JsonFieldAccessor.java   |  41 +-
 .../org/apache/fory/json/reader/JsonReader.java    |  20 +-
 .../apache/fory/json/resolver/CodecRegistry.java   |   8 +-
 .../json/resolver/GeneratedCodecInstantiator.java  | 221 ++++------
 .../json/resolver/GeneratedJsonCodecFactories.java |  98 -----
 .../json/resolver/JsonGeneratedClassRegistry.java  | 174 ++++++++
 .../fory/json/resolver/JsonSharedRegistry.java     | 224 ++++++++--
 .../fory/json/resolver/JsonStringValueCodec.java   |  60 ++-
 .../fory/json/resolver/JsonTypeResolver.java       | 159 ++++++-
 .../fory/json/resolver/JsonValueDeclaration.java   |   5 +-
 .../json/{codec => }/ForyJsonGraalVMFeature.java   | 460 ++++++++++++++++-----
 .../fory-json/native-image.properties              |  16 +-
 .../json/ForyJsonGraalVMFeatureJarVerifier.java    |  48 ++-
 43 files changed, 2093 insertions(+), 793 deletions(-)

diff --git a/ci/run_ci.sh b/ci/run_ci.sh
index 1fddcd744..fe0e9dad9 100755
--- a/ci/run_ci.sh
+++ b/ci/run_ci.sh
@@ -79,7 +79,7 @@ install_jdks() {
   done
 }
 
-run_graalvm_tests() {
+run_graalvm_test() {
   local main_class="$1"
   local java_version
   local java_major
@@ -102,8 +102,8 @@ run_graalvm_tests() {
     -Dmaven.test.skip=true \
     -Dmaven.source.skip=true \
     -Dmaven.javadoc.skip=true
-  echo "Start to build GraalVM JPMS native image for $main_class"
   cd "$ROOT"/integration_tests/graalvm_tests
+  echo "Start to build GraalVM JPMS native image for $main_class"
   mvn -DmainClass="$main_class" -DskipTests=true -Dassembly.skipAssembly=true \
     --no-transfer-progress -Pnative-module clean package
   echo "Built GraalVM JPMS native image"
@@ -113,11 +113,11 @@ run_graalvm_tests() {
 }
 
 graalvm_test() {
-  run_graalvm_tests org.apache.fory.graalvm.Main
+  run_graalvm_test org.apache.fory.graalvm.Main
 }
 
 graalvm_json_tests() {
-  run_graalvm_tests org.apache.fory.graalvm.ForyJsonExample
+  run_graalvm_test org.apache.fory.graalvm.ForyJsonExample
 }
 
 jdk25_access_options() {
diff --git a/docs/guide/java/graalvm-support.md b/docs/guide/java/graalvm-support.md
index a41068a98..824622f7a 100644
--- a/docs/guide/java/graalvm-support.md
+++ b/docs/guide/java/graalvm-support.md
@@ -46,20 +46,8 @@ compilation is unavailable.
 
 ## Fory JSON
 
-Fory JSON uses a separate Native Image workflow. Add the Fory annotation processor to the
-application compiler path:
-
-```xml
-<annotationProcessorPaths>
-  <path>
-    <groupId>org.apache.fory</groupId>
-    <artifactId>fory-annotation-processor</artifactId>
-    <version>${fory.version}</version>
-  </path>
-</annotationProcessorPaths>
-```
-
-Then add `@JsonType` to each concrete object model that the native executable reads or writes:
+Fory JSON has its own Native Image Feature and does not use the Fory annotation processor. Add
+`@JsonType` to each reachable concrete object model that the native executable reads or writes:
 
 ```java
 import org.apache.fory.json.ForyJson;
@@ -80,7 +68,54 @@ public class JsonExample {
 }
 ```
 
-The processor also supports Fory JSON Mixins for models that cannot be modified:
+This is sufficient for correct native execution. During image construction, Fory JSON retains the
+model metadata and prepares its field, property, creator, record, and `JsonAnySetter` access. At
+runtime, `ForyJson.builder().build()` can therefore use interpreted codecs without application
+reflection configuration, package exports or opens, or build-time initialization.
+
+To include generated codecs for a configuration, return that completed configuration from a
+reachable `@ForyJsonProvider`:
+
+```java
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.PropertyNamingStrategy;
+import org.apache.fory.json.annotation.ForyJsonProvider;
+
+@ForyJsonProvider
+public final class JsonConfigs {
+  private final ForyJson api =
+      ForyJson.builder()
+          .writeNullFields(true)
+          .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
+          .registerCodec(Money.class, new MoneyCodec())
+          .build();
+
+  public JsonConfigs() {}
+
+  public ForyJson api() {
+    return api;
+  }
+}
+```
+
+The provider class must be public and concrete and have a public no-argument constructor. Provider
+members are public, non-static, zero-argument instance methods whose exact return type is
+`ForyJson`. Inherited superclass methods and public interface default methods are included. A
+provider may return multiple configurations, and multiple providers may be reachable. Equivalent
+configurations are generated once.
+
+Provider objects exist only while the image is built. Prefer a dedicated configuration class with
+instance fields and methods as shown above; no application `native-image.properties` entry is
+needed, and the provider package does not need to be exported or opened to Fory. Static provider
+methods and fields are not supported.
+
+Only configurations returned by a provider receive generated codecs. The default configuration is
+not generated implicitly. If a codegen-enabled runtime configuration was not included, Fory JSON
+uses its prepared interpreted codecs and logs one process-wide warning recommending a reachable
+`@ForyJsonProvider`. `withCodegen(false)` explicitly selects interpreted codecs and does not request
+generated-codec lookup. Asynchronous compilation is disabled in a native executable.
+
+Use Fory JSON Mixins for models that cannot be modified:
 
 ```java
 import org.apache.fory.json.ForyJson;
@@ -105,40 +140,25 @@ public class JsonExample {
 
 `JsonMixin` is a build-time entry point for its exact declared target, so the target does not need
 `JsonType` solely to use the Mixin. The registered Mixin class literal must be reachable from the
-application. The processor emits available target operations for each non-empty Mixin, and the
-Fory JSON Native Image Feature retains the effective runtime metadata. Normal runtime codec
-precedence still selects the representation. An empty Mixin produces no generated output.
+application. The Native Image Feature retains the target metadata and prepares the same access as
+it does for a direct `JsonType` model. A provider configuration generates the Mixin
+target only when that exact Mixin is registered in the returned `ForyJson`.
 
 Only one source is enabled for an exact target in a built `ForyJson`. Later registration replaces
 an earlier source for subsequent `build()` calls; a runtime keeps the immutable snapshot it was
-built with. If the target also has a direct `JsonType` companion, a non-empty registered Mixin
-selects the pair-specific artifact instead of combining the overlay with the direct companion.
-
-Do not add application reflection configuration as a replacement for the generated configuration.
-The native executable resolves the same effective annotations as the JVM.
-
-The processor generates direct property and creator operations. The `fory-json` artifact activates
-its Native Image Feature automatically and retains the generated factories and required model
-metadata. `@JsonType` is not inherited, so annotate every concrete runtime model. An annotated base
-with a class-literal `@JsonSubTypes` table registers those listed subtypes automatically, but each
-concrete object subtype needs its own direct `@JsonType` to receive generated operations. Reachable
-concrete `Collection` and `Map` root types are also supported when they
-have the public no-argument constructor required by Fory JSON. Reachable `@JsonCodec` declarations
-register their codec constructor even when the declaration target is not an object model. A class
-referenced only by a runtime string is not reachable; `JsonSubTypes.Type.className` is therefore
-unsupported in a native image.
-
-Native execution uses Fory JSON's interpreted readers and writers with the generated property and
-creator operations. `ForyJson.builder()` automatically
-disables runtime code generation and asynchronous compilation in the native executable, while all
-other builder options retain their normal behavior. Applications can create differently configured
-`ForyJson` instances at runtime and do not need build-time initialization or reflection
-configuration.
+built with.
+
+The `fory-json` artifact activates its Native Image Feature automatically. `@JsonType` is not
+inherited, so annotate every concrete runtime model. An annotated base with a class-literal
+`@JsonSubTypes` table registers its listed subtypes automatically. Reachable concrete `Collection`
+and `Map` root types are supported when they have the public no-argument constructor required by
+Fory JSON. A class referenced only by a runtime string is not reachable;
+`JsonSubTypes.Type.className` is therefore unsupported in a native image.
 
 Type, field, effective ordinary getter, setter value parameter, and `JsonCreator` parameter
-`@JsonCodec` annotations are supported. The Feature registers every selected complete-value,
-element, content, Map-key, and Map-value codec constructor. This is the same annotation model used
-on the JVM and Android.
+`@JsonCodec` annotations are supported. The Feature retains every selected complete-value, element,
+content, Map-key, and Map-value codec constructor. This is the same annotation model used on the
+JVM and Android.
 
 `JsonValue` fields and effective public zero-argument methods are supported, including matching
 one-String `JsonCreator` constructors and public static factories. Fixed `JsonRawValue` fields and
@@ -154,21 +174,20 @@ instead.
 `@JsonCodec(valueCodec = ...)` on that field or getter to customize each dynamic value. A second
 `JsonAnySetter` parameter may use the normal configuration for its own value shape.
 
-`JsonUnwrapped` uses the same interpreted behavior as on the JVM. For direct target annotations,
-annotate the containing model and every unwrapped child or intermediate object with `JsonType` so
-each model receives its generated property and creator operations. A Mixin retains the
-unwrapped models reached by its effective schema; register a separate exact Mixin for a child only
-when that child's annotations also need an overlay.
+`JsonUnwrapped` uses the same behavior as on the JVM. For direct target annotations, annotate the
+containing model and every unwrapped child or intermediate object with `JsonType`. A Mixin retains
+the unwrapped models reached by its effective schema; register a separate exact Mixin for a child
+only when that child's annotations also need an overlay.
 
 Child codecs act on one direct level. `elementCodec` supports `Collection`, Java arrays, and
 `AtomicReferenceArray`; `contentCodec` supports `Optional` and `AtomicReference`; `keyCodec` and
 `valueCodec` support Map keys and values. A complete `value` codec cannot be combined with a child
 codec.
 
-An annotation codec must have the same public no-argument constructor required on the JVM. In a
-named module, export or open its package to `org.apache.fory.json`. A codec instance supplied
-through `registerCodec` is constructed by the application and needs no annotation-constructor
-metadata.
+An annotation codec must have a public no-argument constructor. Fory prepares that constructor
+during Native Image construction, so application modules do not need to export or open the codec
+package. A codec instance supplied through `registerCodec` is constructed by the application and
+needs no annotation-constructor metadata.
 
 ## Basic Usage
 
diff --git a/docs/guide/java/json-support.md b/docs/guide/java/json-support.md
index 73b9c9657..e26fe5c35 100644
--- a/docs/guide/java/json-support.md
+++ b/docs/guide/java/json-support.md
@@ -300,8 +300,10 @@ independently to each reader; zero disables the cache, and the setting does not
 input. The buffer setting does not limit output size. Builder changes after `build()` do not mutate
 an existing runtime.
 
-In a GraalVM native image, runtime code generation and asynchronous compilation are automatically
-disabled. Every other builder option keeps the behavior described above.
+In a GraalVM native image, runtime compilation and asynchronous compilation are unavailable.
+Configurations returned by a reachable `ForyJsonProvider` use codecs generated while the image is
+built; other configurations use interpreted codecs with build-time-prepared access metadata. Every
+other builder option keeps the behavior described above.
 
 ## Annotations
 
@@ -335,15 +337,17 @@ import org.apache.fory.json.annotation.JsonUnwrapped;
 ```
 
 `JsonType` asks the annotation processor to generate direct property and creator operations plus
-the exact retention rules for an eligible concrete object model. A directly annotated
-`JsonValue` Record also receives a companion so its value accessor and canonical constructor work
-after Android desugaring. The same generated companion is used on the JVM, Android, and GraalVM
-Native Image. The annotation is not inherited; a concrete subtype needs its own direct annotation
-to receive a companion. See
-[GraalVM Support](graalvm-support.md) and [Android Support](android-support.md) for setup.
-A directly annotated model that uses the default object codec requires that generated companion;
+the exact retention rules for an eligible concrete object model on the JVM and Android. A directly
+annotated `JsonValue` Record also receives a companion so its value accessor and canonical
+constructor work after Android desugaring. The annotation is not inherited; a concrete subtype
+needs its own direct annotation to receive a companion on those runtimes. A directly annotated
+model that uses the default object codec requires that generated companion outside Native Image;
 the runtime reports a configuration error if the processor output is missing.
 
+GraalVM Native Image discovers `JsonType` directly and does not use annotation-processor output.
+See [GraalVM Support](graalvm-support.md) for optional provider-based hosted code generation and
+[Android Support](android-support.md) for annotation-processor setup.
+
 ### Mixins
 
 Use a Mixin to configure an existing class without changing its source:
@@ -427,9 +431,10 @@ Mixin does not introduce a separate record-component model. Use source selectors
 declarations and keep repeated annotations consistent as required by normal record property
 mapping.
 
-On Android and GraalVM Native Image, compile non-empty Mixin sources with the Fory annotation
-processor so required generated operations and platform configuration are available. See
-[Android Support](android-support.md) and [GraalVM Support](graalvm-support.md).
+On Android, compile non-empty Mixin sources with the Fory annotation processor so required
+generated operations and platform configuration are available. GraalVM Native Image discovers
+reachable Mixins directly. See [Android Support](android-support.md) and
+[GraalVM Support](graalvm-support.md).
 
 ### `JsonProperty`
 
@@ -1171,9 +1176,10 @@ no-argument constructor. One instance is shared by all annotated sites and concu
 the built `ForyJson`, so it must be thread-safe. Use `registerCodec(Target.class, instance)` when a
 complete-value codec needs configuration.
 
-In a named Java module, export or open the codec package to `org.apache.fory.json`. When an inherited
-type-declaration codec is used for a more specific target, every decoded value must be null or
-assignable to that target.
+Outside GraalVM Native Image, a named Java module must export or open the codec package to
+`org.apache.fory.json`. Native Image prepares annotation-codec constructors during image
+construction and does not require that package access. When an inherited type-declaration codec is
+used for a more specific target, every decoded value must be null or assignable to that target.
 
 The annotation has the same FIELD, METHOD, and PARAMETER behavior on the JVM, Android, and GraalVM
 Native Image. Ordinary Android classes may omit `JsonType` and provide equivalent exact rules.
diff --git a/integration_tests/graalvm_tests/README.md b/integration_tests/graalvm_tests/README.md
index 3192ebdc6..5462709cd 100644
--- a/integration_tests/graalvm_tests/README.md
+++ b/integration_tests/graalvm_tests/README.md
@@ -1,17 +1,17 @@
 # GraalVM Native Image Tests
 
-Examples and tests for Fory serialization in GraalVM Native Image. The Fory JSON
-entry point covers direct `JsonType` models and runtime registration of exact
-`JsonMixin` target/source mappings. Native-image hosted analysis resolves each mapping and registers
-its generated factory when present. The built executables then execute direct and Mixin mappings at
-runtime; the module-path run verifies the same factories through JPMS.
+Examples and tests for Fory serialization in GraalVM Native Image. The Fory JSON entry point is
+compiled with annotation processing disabled. It covers direct `JsonType` models, exact
+`JsonMixin` target/source mappings, provider-selected hosted codec generation, configuration
+fallback to interpreted codecs, and hosted access metadata for unprovided configurations in one
+native image.
 
 ## Test
 
 ```bash
-mvn clean -DskipTests=true -Pnative package
+mvn -DmainClass=org.apache.fory.graalvm.ForyJsonExample clean -DskipTests=true -Dexec.skip=true -Pnative package
 ./target/main
-mvn clean -DskipTests=true -Pnative-module package
+mvn -DmainClass=org.apache.fory.graalvm.ForyJsonExample clean -DskipTests=true -Pnative-module package
 ./target/main-module
 ```
 
diff --git a/integration_tests/graalvm_tests/pom.xml b/integration_tests/graalvm_tests/pom.xml
index 96b28396d..63b993510 100644
--- a/integration_tests/graalvm_tests/pom.xml
+++ b/integration_tests/graalvm_tests/pom.xml
@@ -95,13 +95,7 @@
         <configuration>
           <source>${maven.compiler.source}</source>
           <target>${maven.compiler.source}</target>
-          <annotationProcessorPaths>
-            <path>
-              <groupId>org.apache.fory</groupId>
-              <artifactId>fory-annotation-processor</artifactId>
-              <version>${project.version}</version>
-            </path>
-          </annotationProcessorPaths>
+          <proc>none</proc>
         </configuration>
       </plugin>
 
diff --git a/integration_tests/graalvm_tests/src/main/java/module-info.java b/integration_tests/graalvm_tests/src/main/java/module-info.java
index e514b7beb..9e5f1f5ab 100644
--- a/integration_tests/graalvm_tests/src/main/java/module-info.java
+++ b/integration_tests/graalvm_tests/src/main/java/module-info.java
@@ -22,7 +22,8 @@ module org.apache.fory.graalvm.tests {
   requires org.apache.fory.json;
   requires java.sql;
 
-  // Fory-generated codecs and annotation codecs access the test models from library modules.
+  // Binary serialization acceptance retains its existing exported and opened model packages.
+  // The Fory JSON closed-package test intentionally uses neither directive.
   exports org.apache.fory.graalvm;
   exports org.apache.fory.graalvm.record;
 
diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
index 4f634d4e9..9003fdca2 100644
--- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
+++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
@@ -19,6 +19,8 @@
 
 package org.apache.fory.graalvm;
 
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.nio.charset.StandardCharsets;
@@ -35,13 +37,20 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.atomic.AtomicReferenceArray;
+import java.util.concurrent.locks.Lock;
+import org.apache.fory.codegen.CompileState;
+import org.apache.fory.graalvm.closed.ClosedJsonConfigs;
 import org.apache.fory.graalvm.closed.ClosedJsonRecord;
 import org.apache.fory.json.ForyJson;
 import org.apache.fory.json.PropertyNamingStrategy;
+import org.apache.fory.json.annotation.ForyJsonProvider;
+import org.apache.fory.json.annotation.JsonAnyGetter;
 import org.apache.fory.json.annotation.JsonAnyProperty;
+import org.apache.fory.json.annotation.JsonAnySetter;
 import org.apache.fory.json.annotation.JsonBase64;
 import org.apache.fory.json.annotation.JsonCodec;
 import org.apache.fory.json.annotation.JsonCreator;
+import org.apache.fory.json.annotation.JsonIgnore;
 import org.apache.fory.json.annotation.JsonMixin;
 import org.apache.fory.json.annotation.JsonProperty;
 import org.apache.fory.json.annotation.JsonPropertyOrder;
@@ -52,42 +61,205 @@ import org.apache.fory.json.annotation.JsonUnwrapped;
 import org.apache.fory.json.annotation.JsonValue;
 import org.apache.fory.json.codec.JsonValueCodec;
 import org.apache.fory.json.codec.MapKeyCodec;
+import org.apache.fory.json.codec.ObjectCodec;
 import org.apache.fory.json.reader.Latin1JsonReader;
 import org.apache.fory.json.reader.Utf16JsonReader;
 import org.apache.fory.json.reader.Utf8JsonReader;
 import org.apache.fory.json.writer.StringJsonWriter;
 import org.apache.fory.json.writer.Utf8JsonWriter;
+import org.apache.fory.platform.GraalvmSupport;
 import org.apache.fory.util.Preconditions;
 
-/** Native-image acceptance coverage for the complete interpreted Fory JSON path. */
+/** Native-image acceptance coverage for hosted code generation and interpreter fallback. */
 public final class ForyJsonExample {
+  private static final String NATIVE_INTERPRETER_MESSAGE =
+      "Fory JSON is using interpreted codecs because the current configuration was not included "
+          + "in this native image. Return this configuration from a reachable "
+          + "@ForyJsonProvider to enable generated codecs.";
+
   private ForyJsonExample() {}
 
   public static void main(String[] args) {
-    testModels();
-    testConfigurations();
-    testCodecs();
-    testValueAnnotations();
-    testSubtypes();
-    testContainerRoots();
-    testGenericProperties();
-    testUnwrapped();
-    testMixin();
-    testMixinValue();
-    testMixinValueRecord();
-    testMixinEnumValue();
-    testMixinCodec();
-    testBigDecimal();
-    testSqlTypes();
-    testClosedPackage();
-    System.out.println("Fory JSON succeed");
+    PrintStream originalOut = System.out;
+    ByteArrayOutputStream captured = new ByteArrayOutputStream();
+    try (PrintStream testOut = new PrintStream(captured, true, StandardCharsets.UTF_8)) {
+      System.setOut(testOut);
+      try {
+        Preconditions.checkArgument(
+            ClosedJsonConfigs.class.isAnnotationPresent(ForyJsonProvider.class));
+        if (GraalvmSupport.isGraalRuntime()) {
+          testHostedCodegenConfigurations();
+        }
+        testModels();
+        testConfigurations();
+        testCodecs();
+        testValueAnnotations();
+        testSubtypes();
+        testContainerRoots();
+        testGenericProperties();
+        testUnwrapped();
+        testMixin();
+        testMixinValue();
+        testMixinValueRecord();
+        testMixinEnumValue();
+        testMixinCodec();
+        testBigDecimal();
+        testSqlTypes();
+        testClosedPackage();
+      } finally {
+        System.setOut(originalOut);
+      }
+    }
+    String output = new String(captured.toByteArray(), StandardCharsets.UTF_8);
+    if (GraalvmSupport.isGraalRuntime()) {
+      int occurrences = countOccurrences(output, NATIVE_INTERPRETER_MESSAGE);
+      Preconditions.checkArgument(
+          occurrences == 1,
+          "Expected one Native Image interpreted-codec message, found "
+              + occurrences
+              + ": "
+              + output);
+    }
+    originalOut.print(output);
+    originalOut.println("Fory JSON succeed");
+  }
+
+  private static void testHostedCodegenConfigurations() {
+    ForyJson providerJson = newProviderJson();
+    ForyJson interpretedJson = newInterpretedJson();
+    exerciseCodegenConfiguration(ForyJson.builder().build(), false);
+    exerciseCodegenConfiguration(providerJson, true);
+    exerciseCodegenConfiguration(interpretedJson, false);
+    testEmptyMixin(providerJson, true);
+    testEmptyMixin(interpretedJson, false);
+    testInterpretedMetadata(interpretedJson);
+    testIndependentChildCodegen();
+    testExternalModuleMixin();
+  }
+
+  private static ForyJson newProviderJson() {
+    return ForyJson.builder()
+        .writeNullFields(true)
+        .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
+        .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec())
+        .registerMixin(CoreCompileStateMixin.class)
+        .registerMixin(EmptyMixin.class)
+        .build();
+  }
+
+  private static ForyJson newInterpretedJson() {
+    return ForyJson.builder()
+        .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
+        .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec())
+        .registerMixin(EmptyMixin.class)
+        .registerMixin(InterpretedMixin.class)
+        .build();
+  }
+
+  private static void testEmptyMixin(ForyJson json, boolean generated) {
+    CodegenProbeCodec.expect(EmptyMixinTarget.class, generated);
+    EmptyMixinTarget value = new EmptyMixinTarget();
+    value.probe = new CodegenProbeValue("empty-mixin");
+    String encoded = json.toJson(value);
+    Preconditions.checkArgument(
+        json.fromJson(encoded, EmptyMixinTarget.class).probe.value.equals("empty-mixin"));
+  }
+
+  private static void testInterpretedMetadata(ForyJson json) {
+    InterpretedMixinTarget mixinValue = new InterpretedMixinTarget();
+    mixinValue.setName("empty-mixin");
+    String mixinJson = json.toJson(mixinValue);
+    Preconditions.checkArgument(
+        json.fromJson(mixinJson, InterpretedMixinTarget.class).getName().equals("empty-mixin"));
+
+    InterpretedBean bean = new InterpretedBean();
+    bean.setName("bean");
+    bean.putExtra("dynamic", "extra");
+    InterpretedBean decoded = json.fromJson(json.toJson(bean), InterpretedBean.class);
+    Preconditions.checkArgument(decoded.getName().equals("bean"));
+    Preconditions.checkArgument(decoded.extra().equals(Map.of("dynamic", "extra")));
+
+    DirectValueRecord record = new DirectValueRecord("record-value");
+    Preconditions.checkArgument(json.toJson(record).equals("\"record-value\""));
+    Preconditions.checkArgument(
+        json.fromJson("\"decoded-record\"", DirectValueRecord.class)
+            .equals(new DirectValueRecord("decoded-record")));
+    Preconditions.checkArgument(json.toJson(DirectValueEnum.READY).equals("\"ready\""));
+    Preconditions.checkArgument(
+        json.fromJson("\"done\"", DirectValueEnum.class) == DirectValueEnum.DONE);
+  }
+
+  private static void exerciseCodegenConfiguration(ForyJson json, boolean generated) {
+    CodegenProbeCodec.expect(CodegenProbeModel.class, generated);
+    CodegenProbeModel value = new CodegenProbeModel();
+    value.id = 41;
+    value.probe = new CodegenProbeValue("probe");
+    value.children.add(new CodegenProbeChild("child"));
+    String encoded = json.toJson(value);
+    Preconditions.checkArgument(encoded.contains("probe"));
+    String utf8 = new String(json.toJsonBytes(value), StandardCharsets.UTF_8);
+    Preconditions.checkArgument(utf8.contains("probe"));
+    Preconditions.checkArgument(
+        json.fromJson(encoded, CodegenProbeModel.class).probe.value.equals("probe"));
+    Preconditions.checkArgument(
+        json.fromJson(encoded, CodegenProbeModel.class).children.get(0).name.equals("child"));
+    String utf16 = encoded.replace(":\"probe\"", ":\"\u4f60\"");
+    Preconditions.checkArgument(
+        json.fromJson(utf16, CodegenProbeModel.class).probe.value.equals("\u4f60"));
+    Preconditions.checkArgument(
+        json.fromJson(utf8.getBytes(StandardCharsets.UTF_8), CodegenProbeModel.class)
+            .probe
+            .value
+            .equals("probe"));
+  }
+
+  private static int countOccurrences(String value, String target) {
+    int count = 0;
+    int offset = 0;
+    while ((offset = value.indexOf(target, offset)) >= 0) {
+      count++;
+      offset += target.length();
+    }
+    return count;
   }
 
   private static void testClosedPackage() {
-    ForyJson json = ForyJson.builder().build();
     ClosedJsonRecord value = new ClosedJsonRecord(17, "closed");
+    ForyJson interpreted = ForyJson.builder().build();
+    String interpretedJson = interpreted.toJson(value);
+    Preconditions.checkArgument(
+        interpreted.fromJson(interpretedJson, ClosedJsonRecord.class).equals(value));
+
+    ForyJson generated = newProviderJson();
+    String generatedJson = generated.toJson(value);
+    Preconditions.checkArgument(
+        generated.fromJson(generatedJson, ClosedJsonRecord.class).equals(value));
+  }
+
+  private static void testIndependentChildCodegen() {
+    CodegenProbeCodec.expect(PublicChild.class, true);
+    ForyJson json = newProviderJson();
+    PackagePrivateOwner value = new PackagePrivateOwner();
+    value.child.name = "child";
+    value.child.probe = new CodegenProbeValue("probe");
     String encoded = json.toJson(value);
-    Preconditions.checkArgument(json.fromJson(encoded, ClosedJsonRecord.class).equals(value));
+    Preconditions.checkArgument(
+        json.fromJson(encoded, PackagePrivateOwner.class).child.name.equals("child"));
+    String utf16 = encoded.replace(":\"probe\"", ":\"你\"");
+    Preconditions.checkArgument(
+        json.fromJson(utf16, PackagePrivateOwner.class).child.probe.value.equals("你"));
+    byte[] utf8 = json.toJsonBytes(value);
+    Preconditions.checkArgument(
+        json.fromJson(utf8, PackagePrivateOwner.class).child.probe.value.equals("probe"));
+  }
+
+  private static void testExternalModuleMixin() {
+    ForyJson json = newProviderJson();
+    CompileState value = new CompileState();
+    value.finished = true;
+    String encoded = json.toJson(value);
+    Preconditions.checkArgument(encoded.equals("{\"finished\":true}"));
+    Preconditions.checkArgument(json.fromJson(encoded, CompileState.class).finished);
   }
 
   private static void testMixin() {
@@ -368,6 +540,182 @@ public final class ForyJsonExample {
     Preconditions.checkArgument(decoded.timestamp.getTime() == 3_000L);
   }
 
+  public interface InheritedJsonConfig {
+    default ForyJson duplicateConfiguration() {
+      return newProviderJson();
+    }
+  }
+
+  @JsonType
+  public static final class CodegenProbeModel {
+    public int id;
+
+    @JsonCodec(CodegenProbeCodec.class)
+    public CodegenProbeValue probe;
+
+    public List<CodegenProbeChild> children = new ArrayList<>();
+
+    public CodegenProbeModel() {}
+  }
+
+  @JsonType
+  public static final class CodegenProbeChild {
+    public String name;
+
+    public CodegenProbeChild() {}
+
+    public CodegenProbeChild(String name) {
+      this.name = name;
+    }
+  }
+
+  public static final class EmptyMixinTarget {
+    @JsonCodec(CodegenProbeCodec.class)
+    private CodegenProbeValue probe;
+
+    public EmptyMixinTarget() {}
+  }
+
+  @JsonMixin(target = EmptyMixinTarget.class)
+  public interface EmptyMixin {}
+
+  public static final class InterpretedMixinTarget {
+    private String name;
+
+    public InterpretedMixinTarget() {}
+
+    public String getName() {
+      return name;
+    }
+
+    public void setName(String name) {
+      this.name = name;
+    }
+  }
+
+  @JsonMixin(target = InterpretedMixinTarget.class)
+  public interface InterpretedMixin {}
+
+  @JsonType
+  public static final class InterpretedBean {
+    private String name;
+    private final transient Map<String, String> extra = new LinkedHashMap<>();
+
+    public InterpretedBean() {}
+
+    public String getName() {
+      return name;
+    }
+
+    public void setName(String name) {
+      this.name = name;
+    }
+
+    @JsonAnyGetter
+    public Map<String, String> extra() {
+      return extra;
+    }
+
+    @JsonAnySetter
+    public void putExtra(String key, String value) {
+      extra.put(key, value);
+    }
+  }
+
+  /** Hosted-only loader which makes the first equivalent provider unable to compile one model. */
+  public static final class CodegenRejectingClassLoader extends ClassLoader {
+    public CodegenRejectingClassLoader() {
+      super(ForyJsonExample.class.getClassLoader());
+    }
+
+    @Override
+    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+      if (name.equals(CodegenProbeModel.class.getName())) {
+        throw new ClassNotFoundException(name);
+      }
+      return super.loadClass(name, resolve);
+    }
+  }
+
+  public static final class CodegenProbeValue {
+    private final String value;
+
+    private CodegenProbeValue(String value) {
+      this.value = value;
+    }
+  }
+
+  public static final class CodegenProbeCodec implements JsonValueCodec<CodegenProbeValue> {
+    private static Class<?> expectedType;
+    private static boolean expectGenerated;
+
+    public CodegenProbeCodec() {}
+
+    private static void expect(Class<?> type, boolean generated) {
+      expectedType = type;
+      expectGenerated = generated;
+    }
+
+    @Override
+    public void writeString(StringJsonWriter writer, CodegenProbeValue value) {
+      checkCapability(writer.typeResolver().getTypeInfo(expectedType, expectedType).stringWriter());
+      writer.writeString(value == null ? null : value.value);
+    }
+
+    @Override
+    public void writeUtf8(Utf8JsonWriter writer, CodegenProbeValue value) {
+      checkCapability(writer.typeResolver().getTypeInfo(expectedType, expectedType).utf8Writer());
+      writer.writeString(value == null ? null : value.value);
+    }
+
+    @Override
+    public CodegenProbeValue readLatin1(Latin1JsonReader reader) {
+      checkCapability(reader.typeResolver().getTypeInfo(expectedType, expectedType).latin1Reader());
+      return reader.tryReadNullToken() ? null : new CodegenProbeValue(reader.readString());
+    }
+
+    @Override
+    public CodegenProbeValue readUtf16(Utf16JsonReader reader) {
+      checkCapability(reader.typeResolver().getTypeInfo(expectedType, expectedType).utf16Reader());
+      return reader.tryReadNullToken() ? null : new CodegenProbeValue(reader.readString());
+    }
+
+    @Override
+    public CodegenProbeValue readUtf8(Utf8JsonReader reader) {
+      checkCapability(reader.typeResolver().getTypeInfo(expectedType, expectedType).utf8Reader());
+      return reader.tryReadNullToken() ? null : new CodegenProbeValue(reader.readString());
+    }
+
+    private static void checkCapability(Object capability) {
+      boolean generated = !(capability instanceof ObjectCodec<?>);
+      Preconditions.checkArgument(generated == expectGenerated);
+    }
+  }
+
+  @JsonType
+  static final class PackagePrivateOwner {
+    public PublicChild child = new PublicChild();
+
+    PackagePrivateOwner() {}
+  }
+
+  @JsonType
+  public static final class PublicChild {
+    public String name;
+
+    @JsonCodec(CodegenProbeCodec.class)
+    public CodegenProbeValue probe;
+
+    public PublicChild() {}
+  }
+
+  @JsonMixin(target = CompileState.class)
+  public abstract static class CoreCompileStateMixin {
+    @JsonIgnore private Lock lock;
+    @JsonProperty private boolean finished;
+    @JsonIgnore private Map<String, byte[]> result;
+  }
+
   public static class Parent {
     private int inheritedId = 10;
 
@@ -565,6 +913,39 @@ public final class ForyJsonExample {
     }
   }
 
+  @JsonType
+  public record DirectValueRecord(@JsonValue String value) {
+    @JsonCreator
+    public DirectValueRecord {}
+  }
+
+  @JsonType
+  public enum DirectValueEnum {
+    READY("ready"),
+    DONE("done");
+
+    private final String value;
+
+    DirectValueEnum(String value) {
+      this.value = value;
+    }
+
+    @JsonValue
+    public String value() {
+      return value;
+    }
+
+    @JsonCreator
+    public static DirectValueEnum fromValue(String value) {
+      for (DirectValueEnum candidate : values()) {
+        if (candidate.value.equals(value)) {
+          return candidate;
+        }
+      }
+      throw new IllegalArgumentException(value);
+    }
+  }
+
   @JsonType
   public static final class RawValue {
     @JsonRawValue public String body;
diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java
new file mode 100644
index 000000000..ec0f4bd5e
--- /dev/null
+++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.graalvm.closed;
+
+import org.apache.fory.graalvm.ForyJsonExample.CodegenProbeCodec;
+import org.apache.fory.graalvm.ForyJsonExample.CodegenProbeValue;
+import org.apache.fory.graalvm.ForyJsonExample.CodegenRejectingClassLoader;
+import org.apache.fory.graalvm.ForyJsonExample.CoreCompileStateMixin;
+import org.apache.fory.graalvm.ForyJsonExample.EmptyMixin;
+import org.apache.fory.graalvm.ForyJsonExample.InheritedJsonConfig;
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.PropertyNamingStrategy;
+import org.apache.fory.json.annotation.ForyJsonProvider;
+
+/** Provider whose constructor and inherited method need no package export or open. */
+@ForyJsonProvider
+public final class ClosedJsonConfigs extends ClosedJsonConfigParent implements InheritedJsonConfig {
+  public ClosedJsonConfigs() {}
+}
+
+class ClosedJsonConfigParent {
+  public ForyJson aRestrictedConfiguration() {
+    return ForyJson.builder()
+        .writeNullFields(true)
+        .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
+        .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec())
+        .registerMixin(CoreCompileStateMixin.class)
+        .registerMixin(EmptyMixin.class)
+        .withClassLoader(new CodegenRejectingClassLoader())
+        .withTypeChecker((className, context) -> false)
+        .build();
+  }
+
+  public ForyJson generatedConfiguration() {
+    return ForyJson.builder()
+        .writeNullFields(true)
+        .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
+        .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec())
+        .registerMixin(CoreCompileStateMixin.class)
+        .registerMixin(EmptyMixin.class)
+        .build();
+  }
+
+  public ForyJson ignoredOverload(boolean ignored) {
+    throw new AssertionError("Provider overload must not be invoked");
+  }
+
+  public static ForyJson ignoredStaticMethod() {
+    throw new AssertionError("Static provider helper must not be invoked");
+  }
+}
diff --git a/java/README.md b/java/README.md
index b0fa0de7b..237b8defd 100644
--- a/java/README.md
+++ b/java/README.md
@@ -472,9 +472,10 @@ annotations, custom codecs, security controls, and platform setup.
 ## GraalVM Native Image
 
 Fory supports GraalVM Native Image without application reflection configuration. Binary
-serialization generates serializers while the image is built; the Fory annotation processor
-generates type-owned execution companions for Fory JSON `@JsonType` models. Build your native image
-as follows:
+serialization generates serializers while the image is built. Fory JSON discovers reachable
+`@JsonType` and `@JsonMixin` models directly, without the annotation processor. Interpreted JSON
+codecs work by default; return configurations from a reachable `@ForyJsonProvider` to generate
+matching JSON codecs while the image is built. Build your native image as follows:
 
 ```bash
 # Generate serializers at build time
diff --git a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedJsonCodecSourceWriter.java b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedJsonCodecSourceWriter.java
index 0d9a6032e..9bec70b35 100644
--- a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedJsonCodecSourceWriter.java
+++ b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedJsonCodecSourceWriter.java
@@ -764,18 +764,7 @@ final class GeneratedJsonCodecSourceWriter {
     if (model.anySetter != null) {
       renderAnySetter(source, model.anySetter);
     }
-    source
-        .append("  public static final class Factory\n")
-        .append("      implements org.apache.fory.json.codec.GeneratedJsonCodecFactory {\n")
-        .append("    public Factory() {}\n\n")
-        .append("    @Override\n")
-        .append("    public org.apache.fory.json.codec.GeneratedJsonCodec<?> create() {\n")
-        .append("      return new ")
-        .append(model.simpleName)
-        .append("();\n")
-        .append("    }\n")
-        .append("  }\n")
-        .append("}\n");
+    source.append("}\n");
     return source.toString();
   }
 
diff --git a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java
index ec2d0d715..4f9c827de 100644
--- a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java
+++ b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java
@@ -57,7 +57,7 @@ import javax.lang.model.util.Types;
 import javax.tools.Diagnostic;
 import javax.tools.StandardLocation;
 
-/** Generates JSON companions and platform configuration for annotated models and Mixins. */
+/** Generates JSON companions and R8 configuration for annotated models and Mixins. */
 final class JsonTypeProcessor {
   private static final String JSON_PACKAGE = "org.apache.fory.json";
   private static final String JSON_TYPE = JSON_PACKAGE + ".annotation.JsonType";
@@ -78,8 +78,6 @@ final class JsonTypeProcessor {
   private static final String NO_MAP_KEY_CODEC = JSON_CODEC + "$NoMapKeyCodec";
   private static final String R8_PREFIX = "META-INF/proguard/fory-json-";
   private static final String R8_MIXIN_PREFIX = "META-INF/proguard/fory-json-mixin-";
-  private static final String NATIVE_IMAGE_PREFIX =
-      "META-INF/native-image/org.apache.fory/fory-json-";
   private static final String[] CODEC_MEMBERS = {
     "value", "elementCodec", "contentCodec", "keyCodec", "valueCodec"
   };
@@ -141,7 +139,6 @@ final class JsonTypeProcessor {
         List<TypeElement> subtypes = classLiteralSubtypes(type, model.binaryFallbackTypes);
         model.sort();
         emitR8(model);
-        emitNativeImageProperties(model);
         pending.addAll(subtypes);
       } catch (GeneratedJsonCodecSourceWriter.InvalidJsonTypeException e) {
         messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), e.element);
@@ -254,7 +251,6 @@ final class JsonTypeProcessor {
         }
         model.sort();
         emitR8(model);
-        emitNativeImageProperties(model);
       } catch (JsonMixinAnnotations.InvalidJsonMixinException e) {
         messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), e.element);
       } catch (GeneratedJsonCodecSourceWriter.InvalidJsonTypeException e) {
@@ -542,28 +538,6 @@ final class JsonTypeProcessor {
     }
   }
 
-  private void emitNativeImageProperties(Model model) {
-    if (model.companionBinaryName == null) {
-      return;
-    }
-    // The hosted feature freezes factory instances into the image heap after reachability is
-    // known, but GraalVM accepts class-initialization configuration only before analysis starts.
-    // Emit the exact generated class here so unreachable model classes remain removable.
-    String resourceName =
-        NATIVE_IMAGE_PREFIX + model.companionBinaryName + "/native-image.properties";
-    try {
-      javax.tools.FileObject file =
-          filer.createResource(
-              StandardLocation.CLASS_OUTPUT, "", resourceName, model.originatingElements());
-      try (Writer writer = file.openWriter()) {
-        writer.write("Args=--initialize-at-build-time=" + model.companionBinaryName + "$Factory\n");
-      }
-    } catch (IOException e) {
-      throw new InvalidJsonTypeException(
-          "Failed to write generated JSON Native Image properties: " + e, model.target);
-    }
-  }
-
   private String writeR8(Model model) {
     StringBuilder builder = new StringBuilder(8192);
     builder.append("-keepattributes Signature,RuntimeVisibleAnnotations\n");
diff --git a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java
index 0c19450e7..80bfa2edb 100644
--- a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java
+++ b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java
@@ -49,7 +49,6 @@ import javax.tools.ToolProvider;
 import org.apache.fory.json.ForyJson;
 import org.apache.fory.json.ForyJsonException;
 import org.apache.fory.json.codec.GeneratedJsonCodec;
-import org.apache.fory.json.codec.GeneratedJsonCodecFactory;
 import org.apache.fory.json.meta.JsonAnySetterAccessor;
 import org.apache.fory.json.meta.JsonFieldAccessor;
 import org.testng.SkipException;
@@ -58,8 +57,6 @@ import org.testng.annotations.Test;
 public class JsonTypeProcessorTest {
   private static final String RULE_PREFIX = "META-INF/proguard/fory-json-";
   private static final String MIXIN_RULE_PREFIX = "META-INF/proguard/fory-json-mixin-";
-  private static final String NATIVE_IMAGE_PREFIX =
-      "META-INF/native-image/org.apache.fory/fory-json-";
 
   @Test
   public void unannotatedType() throws Exception {
@@ -82,8 +79,6 @@ public class JsonTypeProcessorTest {
     String companion = "test.EmptyMixin_ForyJsonMixin_test_x2e_Target_ForyJsonCodec";
     assertFalse(result.hasGeneratedSource(companion.replace('.', '/') + ".java"));
     assertFalse(result.hasGeneratedResource(MIXIN_RULE_PREFIX + "test.EmptyMixin.pro"));
-    assertFalse(
-        result.hasGeneratedResource(NATIVE_IMAGE_PREFIX + companion + "/native-image.properties"));
   }
 
   @Test
@@ -505,10 +500,6 @@ public class JsonTypeProcessorTest {
     assertFalse(rules.contains("test.Plain_ForyJsonCodec$Factory"), rules);
     assertFalse(rules.contains("-keep,allowoptimization,allowobfuscation class test.Plain"), rules);
     assertTrue(result.hasGeneratedSource("test/Plain_ForyJsonCodec.java"));
-    assertEquals(
-        result.generatedResource(
-            NATIVE_IMAGE_PREFIX + "test.Plain_ForyJsonCodec/native-image.properties"),
-        "Args=--initialize-at-build-time=test.Plain_ForyJsonCodec$Factory\n");
   }
 
   @Test
@@ -553,14 +544,6 @@ public class JsonTypeProcessorTest {
     anySetter.put(model, "answer", 42);
     Field extra = modelType.getField("extra");
     assertEquals(((Map<?, ?>) extra.get(model)).get("answer"), 42);
-
-    GeneratedJsonCodecFactory factory =
-        (GeneratedJsonCodecFactory)
-            loader
-                .loadClass("test.GeneratedModel_ForyJsonCodec$Factory")
-                .getConstructor()
-                .newInstance();
-    assertEquals(factory.create().type(), modelType);
   }
 
   @Test
diff --git a/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java b/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java
index adfd99a64..cf490bd1a 100644
--- a/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java
+++ b/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java
@@ -49,10 +49,11 @@ public class DefineClass {
     Preconditions.checkNotNull(loader);
     Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 8);
     if (neighbor != null && JdkVersion.MAJOR_VERSION >= 9) {
-      // classes in bytecode must be in same package as lookup class.
-      MethodHandles.Lookup lookup = MethodHandles.lookup();
-      _JDKAccess.addReads(_JDKAccess.getModule(DefineClass.class), _JDKAccess.getModule(neighbor));
-      lookup = _Lookup.privateLookupIn(neighbor, lookup);
+      // A normal privateLookupIn would make ordinary generated classes depend on the application
+      // package being open to Fory. The target-class trusted lookup defines the class in the
+      // neighbor's loader, runtime package, module, and protection domain without that user-facing
+      // module contract. Classes in bytecode must still be in the neighbor's package.
+      MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(neighbor);
       return _Lookup.defineClass(lookup, bytecodes);
     }
     if (classloaderDefineClassHandle == null) {
diff --git a/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java
index c989207e2..d88bba3cf 100644
--- a/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java
+++ b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java
@@ -345,18 +345,25 @@ public class _JDKAccess {
     }
   }
 
-  // caller sensitive, must use MethodHandle to walk around the check.
   private static volatile MethodHandle addReadsHandle;
 
   public static Object addReads(Object thisModule, Object otherModule) {
     Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 9);
     try {
       if (addReadsHandle == null) {
-        Class<?> cls = Class.forName("java.lang.Module");
-        MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(cls);
-        addReadsHandle = lookup.findVirtual(cls, "addReads", MethodType.methodType(cls, cls));
+        Class<?> moduleClass = Class.forName("java.lang.Module");
+        Class<?> modulesClass = Class.forName("jdk.internal.module.Modules");
+        MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(modulesClass);
+        // Module.addReads is caller-sensitive and rejects changes to another module even through a
+        // trusted handle. The JDK module-graph owner exposes the corresponding privileged update.
+        addReadsHandle =
+            lookup.findStatic(
+                modulesClass,
+                "addReads",
+                MethodType.methodType(void.class, moduleClass, moduleClass));
       }
-      return addReadsHandle.invoke(thisModule, otherModule);
+      addReadsHandle.invoke(thisModule, otherModule);
+      return thisModule;
     } catch (Throwable e) {
       throw ExceptionUtils.throwException(e);
     }
diff --git a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
index f5e5f2c88..8fd42e1fa 100644
--- a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
+++ b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
@@ -263,7 +263,10 @@ Args=--features=org.apache.fory.platform.ForyGraalVMFeature \
     org.apache.fory.reflect.TypeRef$ClassOwnership$1,\
     org.apache.fory.reflect.TypeRef$ClassOwnership$2,\
     org.apache.fory.reflect.TypeRef$ClassOwnership,\
+    org.apache.fory.reflect.TypeRef$GenericArrayTypeImpl,\
+    org.apache.fory.reflect.TypeRef$ParameterizedTypeImpl,\
     org.apache.fory.reflect.TypeRef$TypeVariableKey,\
+    org.apache.fory.reflect.TypeRef$WildcardTypeImpl,\
     org.apache.fory.reflect.TypeRef,\
     org.apache.fory.reflect.ObjectInstantiators,\
     org.apache.fory.reflect.ObjectInstantiators$DeclaredNoArgCtrInstantiator,\
diff --git a/java/fory-json/README.md b/java/fory-json/README.md
index eb7aa1d08..33ba4942c 100644
--- a/java/fory-json/README.md
+++ b/java/fory-json/README.md
@@ -376,8 +376,11 @@ transport boundary when parsing untrusted input.
 
 Builder mutation after `build()` does not modify an existing `ForyJson` runtime.
 
-On Android and in a GraalVM native image, runtime code generation and asynchronous compilation are
-automatically disabled. Every other builder option keeps the behavior described above.
+On Android, runtime code generation and asynchronous compilation are disabled. In a GraalVM native
+image, runtime compilation is unavailable; configurations returned by a reachable
+`ForyJsonProvider` use codecs generated while the image is built, and other configurations use
+interpreted codecs with build-time-prepared access metadata. Every other builder option keeps the
+behavior described above.
 
 ## JSON annotations
 
@@ -388,13 +391,15 @@ and `JsonValue`. `JsonType` is a separate build-time generation marker. They are
 not Jackson, Gson, or Fory binary-protocol compatibility annotations.
 
 `JsonType` asks the annotation processor to generate direct property and creator operations plus
-exact retention rules. It is not inherited, so annotate each eligible concrete model that needs a
-generated companion. A directly annotated `JsonValue` Record also receives a companion for its
-value accessor and canonical constructor. Ordinary unannotated classes may still use reflection; on
-Android they need application-authored exact R8 rules. Android-desugared Records require
-processor-generated operations from either a direct `JsonType` declaration or a compiled exact
-`JsonMixin` pair. A directly annotated model that uses the default object codec fails during codec
-creation if its generated companion is missing.
+exact retention rules on the JVM and Android. It is not inherited, so annotate each eligible
+concrete model that needs a generated companion on those runtimes. A directly annotated
+`JsonValue` Record also receives a companion for its value accessor and canonical constructor.
+Ordinary unannotated classes may still use reflection; on Android they need application-authored
+exact R8 rules. Android-desugared Records require processor-generated operations from either a
+direct `JsonType` declaration or a compiled exact `JsonMixin` pair. Outside Native Image, a
+directly annotated model that uses the default object codec fails during codec creation if its
+generated companion is missing. GraalVM Native Image discovers `JsonType` directly and does not use
+annotation-processor output.
 See the [GraalVM guide](../../docs/guide/java/graalvm-support.md) and
 [Android guide](../../docs/guide/java/android-support.md) for the platform workflows.
 
@@ -454,9 +459,9 @@ A `JsonCodec` supplied by a Mixin is the target's effective annotation. An exact
 `registerCodec` registration still wins, while the effective type annotation wins over a built-in
 mapping.
 
-On Android and GraalVM Native Image, compile non-empty Mixins with the Fory annotation processor
-so required generated operations and platform configuration are available. See the platform guides
-linked above.
+On Android, compile non-empty Mixins with the Fory annotation processor so required generated
+operations and platform configuration are available. GraalVM Native Image discovers reachable
+Mixins directly. See the platform guides linked above.
 
 ### `JsonProperty`
 
@@ -1290,9 +1295,10 @@ no-argument constructor. One instance is shared by all annotated sites and concu
 the built `ForyJson`, so it must be thread-safe. Use `registerCodec(Target.class, instance)` when a
 complete-value codec needs configuration.
 
-In a named Java module, export or open the codec package to `org.apache.fory.json`. When an inherited
-type-declaration codec is used for a more specific target, every decoded value must be null or
-assignable to that target.
+Outside GraalVM Native Image, a named Java module must export or open the codec package to
+`org.apache.fory.json`. Native Image prepares annotation-codec constructors during image
+construction and does not require that package access. When an inherited type-declaration codec is
+used for a more specific target, every decoded value must be null or assignable to that target.
 
 The annotation has the same FIELD, METHOD, and PARAMETER behavior on the JVM, Android, and GraalVM
 Native Image. Ordinary Android classes may omit `JsonType` and provide equivalent exact rules.
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java
index d4841a637..bc4112bf4 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java
@@ -83,6 +83,7 @@ public final class ForyJson {
   public static final int DEFAULT_MAX_CACHED_FIELD_NAMES = 8192;
 
   private final int homeSlotMask;
+  private final JsonConfig config;
   private final PooledState[] slots;
 
   ForyJson(JsonConfig config) {
@@ -90,6 +91,7 @@ public final class ForyJson {
   }
 
   ForyJson(JsonConfig config, JsonSharedRegistry sharedRegistry) {
+    this.config = config;
     int poolSize = config.concurrencyLevel();
     homeSlotMask = Integer.highestOneBit(poolSize) - 1;
     // This fixed array is the only JsonState owner. Each state's three readers own their configured
@@ -106,6 +108,10 @@ public final class ForyJson {
     return new ForyJsonBuilder();
   }
 
+  JsonConfig config() {
+    return config;
+  }
+
   /**
    * Serializes {@code value} as one complete JSON document backed by a detached String.
    *
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
index 102b36b0a..f9535197a 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
@@ -76,7 +76,9 @@ public final class ForyJsonBuilder {
 
   /**
    * Enables generated object codecs for supported classes. Enabled by default and automatically
-   * disabled on Android and in a GraalVM native image.
+   * disabled on Android. In a GraalVM native image, generated codecs are available only for
+   * configurations returned by a reachable {@link
+   * org.apache.fory.json.annotation.ForyJsonProvider}; other configurations use interpreted codecs.
    */
   public ForyJsonBuilder withCodegen(boolean codegenEnabled) {
     this.codegenEnabled = codegenEnabled;
@@ -248,9 +250,9 @@ public final class ForyJsonBuilder {
         fixedClassLoader = ForyJson.class.getClassLoader();
       }
     }
-    boolean effectiveCodegen =
-        codegenEnabled && !AndroidSupport.IS_ANDROID && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE;
-    boolean effectiveAsyncCompilation = asyncCompilationEnabled && effectiveCodegen;
+    boolean effectiveCodegen = codegenEnabled && !AndroidSupport.IS_ANDROID;
+    boolean effectiveAsyncCompilation =
+        asyncCompilationEnabled && effectiveCodegen && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE;
     return new ForyJson(
         new JsonConfig(
             writeNullFields,
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java b/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java
index 2d82636d6..c2477fb01 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java
@@ -30,17 +30,17 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.fory.annotation.Internal;
+import org.apache.fory.json.codegen.JsonCodegenKey;
 import org.apache.fory.json.resolver.CodecRegistry;
 
 /**
  * Build configuration used to create all pooled states of one {@link ForyJson} instance.
  *
- * <p>Scalar settings are fixed at construction. The codec registry is builder-owned mutable input
- * and is copied immediately by the runtime's shared registry; the JSON runtime never mutates it.
- * {@link #getCodegenHash()} identifies only settings that can change generated source; runtime-only
- * settings such as depth and asynchronous scheduling do not fragment generated class names.
- * Concurrency, per-reader field-name cache, and retained writer-buffer limits are also runtime-only
- * and do not fragment generated class names.
+ * <p>Scalar settings and the codec registry are snapshotted at construction; the JSON runtime never
+ * observes later builder mutation. {@link #getCodegenHash()} identifies only settings that can
+ * change generated source; runtime-only settings such as depth and asynchronous scheduling do not
+ * fragment generated class names. Concurrency, per-reader field-name cache, and retained
+ * writer-buffer limits are also runtime-only and do not fragment generated class names.
  */
 public final class JsonConfig {
   private static final int MAX_CACHED_FIELD_NAMES = 1 << 29;
@@ -59,7 +59,7 @@ public final class JsonConfig {
   private final Map<Class<?>, Class<?>> mixins;
   private final JsonTypeChecker typeChecker;
   private final JsonTypeCheckContext typeCheckContext;
-  private final CodegenKey codegenKey;
+  private final JsonCodegenKey codegenKey;
   private transient int codegenHash;
 
   JsonConfig(
@@ -88,13 +88,13 @@ public final class JsonConfig {
     this.maxCachedFieldNames = maxCachedFieldNames;
     this.concurrencyLevel = concurrencyLevel;
     this.bufferSizeLimitBytes = bufferSizeLimitBytes;
-    this.codecRegistry = codecRegistry;
+    this.codecRegistry = codecRegistry.copy();
     this.mixins = immutableMixins(mixins);
     this.typeChecker = typeChecker;
     typeCheckContext = new JsonTypeCheckContext();
-    String codecRegistryKey = codecRegistry.codegenKey();
+    String codecRegistryKey = this.codecRegistry.codegenKey();
     codegenKey =
-        new CodegenKey(
+        new JsonCodegenKey(
             writeNullFields,
             propertyDiscoveryEnabled,
             propertyNamingStrategy,
@@ -200,7 +200,7 @@ public final class JsonConfig {
   // Equal generated source inputs share one map entry, following core generated-code naming model.
   // This process-wide map retains only immutable configuration text and integers, never user
   // classes, codec instances, class loaders, or generated classes.
-  private static final ConcurrentMap<CodegenKey, Integer> CODEGEN_ID_MAP =
+  private static final ConcurrentMap<JsonCodegenKey, Integer> CODEGEN_ID_MAP =
       new ConcurrentHashMap<>();
 
   public int getCodegenHash() {
@@ -210,48 +210,9 @@ public final class JsonConfig {
     return codegenHash;
   }
 
-  private static final class CodegenKey {
-    private final boolean writeNullFields;
-    private final boolean propertyDiscoveryEnabled;
-    private final PropertyNamingStrategy propertyNamingStrategy;
-    private final String codecRegistryKey;
-    private final String mixinKey;
-
-    private CodegenKey(
-        boolean writeNullFields,
-        boolean propertyDiscoveryEnabled,
-        PropertyNamingStrategy propertyNamingStrategy,
-        String codecRegistryKey,
-        String mixinKey) {
-      this.writeNullFields = writeNullFields;
-      this.propertyDiscoveryEnabled = propertyDiscoveryEnabled;
-      this.propertyNamingStrategy = propertyNamingStrategy;
-      this.codecRegistryKey = codecRegistryKey;
-      this.mixinKey = mixinKey;
-    }
-
-    @Override
-    public boolean equals(Object other) {
-      if (this == other) {
-        return true;
-      }
-      if (other == null || getClass() != other.getClass()) {
-        return false;
-      }
-      CodegenKey that = (CodegenKey) other;
-      return writeNullFields == that.writeNullFields
-          && propertyDiscoveryEnabled == that.propertyDiscoveryEnabled
-          && propertyNamingStrategy == that.propertyNamingStrategy
-          && Objects.equals(codecRegistryKey, that.codecRegistryKey)
-          && Objects.equals(mixinKey, that.mixinKey);
-    }
-
-    @Override
-    public int hashCode() {
-      int result =
-          Objects.hash(
-              writeNullFields, propertyDiscoveryEnabled, propertyNamingStrategy, codecRegistryKey);
-      return 31 * result + mixinKey.hashCode();
-    }
+  /** Returns the immutable generated-source identity for this configuration. */
+  @Internal
+  public JsonCodegenKey codegenKey() {
+    return codegenKey;
   }
 }
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/annotation/ForyJsonProvider.java b/java/fory-json/src/main/java/org/apache/fory/json/annotation/ForyJsonProvider.java
new file mode 100644
index 000000000..5384d872c
--- /dev/null
+++ b/java/fory-json/src/main/java/org/apache/fory/json/annotation/ForyJsonProvider.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.json.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.fory.json.ForyJson;
+
+/**
+ * Supplies Fory JSON configurations for GraalVM Native Image hosted code generation.
+ *
+ * <p>Annotate a reachable public concrete class with a public no-argument constructor. Every
+ * effective public, non-static, zero-argument instance method whose exact return type is {@link
+ * ForyJson} is invoked once while the native image is built. This includes inherited superclass
+ * methods and public interface default methods. The returned configurations select the generated
+ * object codecs included in the image; configurations not returned by a provider continue to use
+ * interpreted codecs. The provider package does not need to be exported or opened to Fory.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ForyJsonProvider {}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java
index 743b35957..7ac3f672b 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java
@@ -31,11 +31,12 @@ import java.lang.annotation.Target;
  * <p>The Fory annotation processor generates a type-owned JSON companion for an eligible concrete
  * object model or a Record with an effective {@link JsonValue}, together with exact R8 rules for
  * the model, companion, and codec classes selected by its {@link JsonCodec} declarations. The
- * companion provides direct member access and creator invocation on the JVM, Android, and GraalVM
- * Native Image. A concrete subtype listed only by a class-literal {@link JsonSubTypes} entry
- * receives retention metadata but needs its own direct {@code JsonType} annotation to receive a
- * companion. A directly annotated model that reaches the default object codec fails during codec
- * creation when its generated companion is missing.
+ * companion provides direct member access and creator invocation on the JVM and Android. GraalVM
+ * Native Image discovers this annotation directly and does not use annotation-processor output. A
+ * concrete subtype listed only by a class-literal {@link JsonSubTypes} entry receives retention
+ * metadata but needs its own direct {@code JsonType} annotation to receive a companion on the JVM
+ * or Android. Outside Native Image, a directly annotated model that reaches the default object
+ * codec fails during codec creation when its generated companion is missing.
  *
  * <p>This annotation does not change the JSON schema and is intentionally not inherited. An
  * ordinary mutable class may omit it and use reflection, with application-authored exact R8 rules
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
index 034a0dea2..1be0b0843 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
@@ -19,6 +19,7 @@
 
 package org.apache.fory.json.codec;
 
+import java.lang.invoke.MethodHandle;
 import java.lang.reflect.Type;
 import java.math.BigDecimal;
 import java.math.BigInteger;
@@ -54,6 +55,8 @@ import org.apache.fory.json.resolver.JsonTypeResolver;
 import org.apache.fory.json.writer.JsonWriter;
 import org.apache.fory.json.writer.StringJsonWriter;
 import org.apache.fory.json.writer.Utf8JsonWriter;
+import org.apache.fory.platform.GraalvmSupport;
+import org.apache.fory.reflect.ReflectionUtils;
 import org.apache.fory.reflect.TypeRef;
 
 /**
@@ -261,6 +264,16 @@ public abstract class CollectionCodec<T extends Collection<?>> implements JsonVa
       }
       return CollectionFactory.ARRAY_LIST;
     }
+    if (GraalvmSupport.isGraalRuntime()) {
+      MethodHandle constructor = ReflectionUtils.getCtrHandle(rawType, new Class<?>[0]);
+      return () -> {
+        try {
+          return (Collection<Object>) constructor.invoke();
+        } catch (Throwable e) {
+          throw new ForyJsonException("Cannot create collection " + rawType, e);
+        }
+      };
+    }
     return () -> {
       try {
         return (Collection<Object>) rawType.newInstance();
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedJsonCodecFactory.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedJsonCodecFactory.java
deleted file mode 100644
index 35a0eb15c..000000000
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedJsonCodecFactory.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.fory.json.codec;
-
-import org.apache.fory.annotation.Internal;
-
-/** Stateless GraalVM runtime factory for a generated JSON companion. */
-@Internal
-public interface GeneratedJsonCodecFactory {
-  GeneratedJsonCodec<?> create();
-}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
index 305df441b..74b6bdde3 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
@@ -19,6 +19,7 @@
 
 package org.apache.fory.json.codec;
 
+import java.lang.invoke.MethodHandle;
 import java.lang.reflect.Type;
 import java.math.BigDecimal;
 import java.math.BigInteger;
@@ -46,6 +47,8 @@ import org.apache.fory.json.resolver.JsonTypeResolver;
 import org.apache.fory.json.writer.JsonWriter;
 import org.apache.fory.json.writer.StringJsonWriter;
 import org.apache.fory.json.writer.Utf8JsonWriter;
+import org.apache.fory.platform.GraalvmSupport;
+import org.apache.fory.reflect.ReflectionUtils;
 import org.apache.fory.reflect.TypeRef;
 
 /**
@@ -315,6 +318,16 @@ public abstract class MapCodec<T extends Map<?, ?>> implements JsonValueCodec<T>
       }
       return () -> new LinkedHashMap<>(0);
     }
+    if (GraalvmSupport.isGraalRuntime()) {
+      MethodHandle constructor = ReflectionUtils.getCtrHandle(rawType, new Class<?>[0]);
+      return () -> {
+        try {
+          return (Map<Object, Object>) constructor.invoke();
+        } catch (Throwable e) {
+          throw new ForyJsonException("Cannot create map " + rawType, e);
+        }
+      };
+    }
     return () -> {
       try {
         return (Map<Object, Object>) rawType.newInstance();
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java
index ace30ddc9..88f2b607d 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java
@@ -26,7 +26,10 @@ import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.lang.reflect.Type;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import org.apache.fory.annotation.Internal;
+import org.apache.fory.collection.ClassValueCache;
 import org.apache.fory.json.ForyJsonException;
 import org.apache.fory.json.PropertyNamingStrategy;
 import org.apache.fory.json.annotation.JsonCodec;
@@ -50,6 +53,7 @@ import org.apache.fory.json.resolver.JsonTypeResolver;
 import org.apache.fory.json.writer.StringJsonWriter;
 import org.apache.fory.json.writer.Utf8JsonWriter;
 import org.apache.fory.platform.AndroidSupport;
+import org.apache.fory.platform.GraalvmSupport;
 import org.apache.fory.platform.internal._JDKAccess;
 import org.apache.fory.reflect.ObjectInstantiator;
 import org.apache.fory.reflect.TypeRef;
@@ -156,6 +160,24 @@ public class ObjectCodec<T> implements JsonValueCodec<T> {
         instantiator);
   }
 
+  /** Returns whether hosted metadata must retain this method for object discovery. */
+  @Internal
+  public static boolean usesJsonMetadata(Method method, boolean record) {
+    return ObjectCodecBuilder.usesJsonMetadata(method, record);
+  }
+
+  /** Returns whether hosted metadata must retain this method's return type. */
+  @Internal
+  public static boolean usesJsonReturn(Method method) {
+    return ObjectCodecBuilder.usesJsonReturn(method);
+  }
+
+  /** Returns whether hosted metadata must retain this method's parameter types. */
+  @Internal
+  public static boolean usesJsonParameters(Method method) {
+    return ObjectCodecBuilder.usesJsonParameters(method);
+  }
+
   public final Class<?> type() {
     return type;
   }
@@ -1392,6 +1414,11 @@ public class ObjectCodec<T> implements JsonValueCodec<T> {
 
   @Internal
   public static final class AnyInfo {
+    // Hosted discovery and runtime codec construction both resolve Any setters through
+    // anySetterHandle, so the image heap owns the single cache of prepared handles.
+    private static final ClassValueCache<ConcurrentMap<Method, MethodHandle>>
+        NATIVE_SETTER_HANDLES = ClassValueCache.newClassKeyCache(32);
+
     private final Field writeField;
     private final Method writeGetter;
     private final Field readField;
@@ -1439,7 +1466,7 @@ public class ObjectCodec<T> implements JsonValueCodec<T> {
       setterHandle =
           readSetter == null || generatedSetter != null || AndroidSupport.IS_ANDROID
               ? null
-              : methodHandle(readSetter);
+              : anySetterHandle(readSetter);
       if (readSetter != null && generatedSetter == null && AndroidSupport.IS_ANDROID) {
         readSetter.setAccessible(true);
       }
@@ -1565,7 +1592,18 @@ public class ObjectCodec<T> implements JsonValueCodec<T> {
       }
     }
 
-    private static MethodHandle methodHandle(Method method) {
+    /** Returns the invocation handle for one {@code JsonAnySetter} method. */
+    @Internal
+    public static MethodHandle anySetterHandle(Method method) {
+      if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+        ConcurrentMap<Method, MethodHandle> handles =
+            NATIVE_SETTER_HANDLES.get(method.getDeclaringClass(), ConcurrentHashMap::new);
+        return handles.computeIfAbsent(method, AnyInfo::newAnySetterHandle);
+      }
+      return newAnySetterHandle(method);
+    }
+
+    private static MethodHandle newAnySetterHandle(Method method) {
       try {
         return _JDKAccess._trustedLookup(method.getDeclaringClass()).unreflect(method);
       } catch (IllegalAccessException e) {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
index a872d05f9..89e2cfd1f 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
@@ -63,6 +63,7 @@ import org.apache.fory.json.meta.JsonFieldAccessor;
 import org.apache.fory.json.meta.JsonFieldInfo;
 import org.apache.fory.json.meta.JsonFieldNameHash;
 import org.apache.fory.json.resolver.JsonSharedRegistry;
+import org.apache.fory.platform.GraalvmSupport;
 import org.apache.fory.reflect.ObjectInstantiator;
 import org.apache.fory.reflect.ObjectInstantiators;
 import org.apache.fory.reflect.TypeRef;
@@ -340,7 +341,11 @@ final class ObjectCodecBuilder {
                 annotations)
             : null;
     ObjectInstantiator<?> instantiator =
-        creatorInfo == null ? ObjectInstantiators.createObjectInstantiator(type) : null;
+        creatorInfo == null
+            ? GraalvmSupport.isGraalRuntime()
+                ? ObjectInstantiators.getObjectInstantiator(type)
+                : ObjectInstantiators.createObjectInstantiator(type)
+            : null;
     String[] skipped = hasAny ? skippedNames.toArray(new String[0]) : null;
     JsonUnwrappedInfo unwrappedInfo =
         hasUnwrapped
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/SqlJsonCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/SqlJsonCodecs.java
index 3344aacba..2b471fe26 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/SqlJsonCodecs.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/SqlJsonCodecs.java
@@ -19,6 +19,7 @@
 
 package org.apache.fory.json.codec;
 
+import java.lang.invoke.MethodHandle;
 import java.lang.reflect.Constructor;
 import java.util.Date;
 import java.util.IdentityHashMap;
@@ -28,6 +29,8 @@ import org.apache.fory.json.reader.Utf16JsonReader;
 import org.apache.fory.json.reader.Utf8JsonReader;
 import org.apache.fory.json.writer.StringJsonWriter;
 import org.apache.fory.json.writer.Utf8JsonWriter;
+import org.apache.fory.platform.GraalvmSupport;
+import org.apache.fory.reflect.ReflectionUtils;
 
 /**
  * Optional codecs for {@code java.sql} date/time values represented as epoch milliseconds.
@@ -67,13 +70,22 @@ public final class SqlJsonCodecs {
   }
 
   private static final class SqlMillisCodec<T extends Date> implements JsonValueCodec<T> {
+    private final Class<T> type;
     private final Constructor<T> constructor;
+    private final MethodHandle constructorHandle;
 
     private SqlMillisCodec(Class<T> type) {
-      try {
-        constructor = type.getConstructor(long.class);
-      } catch (NoSuchMethodException e) {
-        throw new ForyJsonException("Cannot access SQL JSON type " + type.getName(), e);
+      this.type = type;
+      if (GraalvmSupport.isGraalRuntime()) {
+        constructor = null;
+        constructorHandle = ReflectionUtils.getCtrHandle(type, long.class);
+      } else {
+        try {
+          constructor = type.getConstructor(long.class);
+          constructorHandle = null;
+        } catch (NoSuchMethodException e) {
+          throw new ForyJsonException("Cannot access SQL JSON type " + type.getName(), e);
+        }
       }
     }
 
@@ -110,12 +122,14 @@ public final class SqlJsonCodecs {
       return reader.tryReadNullToken() ? null : newSqlValue(reader.readLong());
     }
 
+    @SuppressWarnings("unchecked")
     private T newSqlValue(long millis) {
       try {
-        return constructor.newInstance(millis);
-      } catch (ReflectiveOperationException e) {
-        throw new ForyJsonException(
-            "Cannot create SQL JSON type " + constructor.getDeclaringClass(), e);
+        return constructorHandle == null
+            ? constructor.newInstance(millis)
+            : (T) constructorHandle.invoke(millis);
+      } catch (Throwable e) {
+        throw new ForyJsonException("Cannot create SQL JSON type " + type, e);
       }
     }
   }
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 68aa8690c..99b6735a7 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
@@ -52,6 +52,8 @@ import org.apache.fory.json.meta.JsonFieldInfo;
 import org.apache.fory.json.meta.JsonFieldKind;
 import org.apache.fory.json.resolver.JsonTypeInfo;
 import org.apache.fory.json.resolver.JsonTypeResolver;
+import org.apache.fory.platform.internal.DefineClass;
+import org.apache.fory.platform.internal._JDKAccess;
 
 /**
  * Generates concrete object and exact-collection capability classes.
@@ -79,6 +81,7 @@ public final class JsonCodegen {
   private final int codegenHash;
   private final CodeGenerator codeGenerator;
   private final ClassLoader jsonLoader;
+  private final boolean hostedCodegen;
 
   static String generatedCodecType(CodegenContext ctx, Class<?> codecType) {
     // Janino-generated serializers use erased types, matching Fory core code generation. Runtime
@@ -91,9 +94,10 @@ public final class JsonCodegen {
     return ctx.type(arrayType);
   }
 
-  public JsonCodegen(int codegenHash, ClassLoader jsonLoader) {
+  public JsonCodegen(int codegenHash, ClassLoader jsonLoader, boolean hostedCodegen) {
     this.codegenHash = codegenHash;
     this.jsonLoader = jsonLoader;
+    this.hostedCodegen = hostedCodegen;
     codeGenerator = new CodeGenerator(jsonLoader);
   }
 
@@ -218,7 +222,7 @@ public final class JsonCodegen {
       String code =
           new StringWriterCodegen(this, resolver)
               .genUnwrappedWriterCode(builder, type, codec, unwrapped);
-      return compileCodecClass(generatedPackage, className, code);
+      return compileObjectCodecClass(type, generatedPackage, className, code);
     }
     AnyInfo any = codec.anyInfo();
     JsonFieldInfo[] properties = codec.writeFields();
@@ -227,7 +231,7 @@ public final class JsonCodegen {
           new JsonGeneratedCodecBuilder(generatedPackage, className, type);
       String code =
           new StringWriterCodegen(this, resolver).genAnyWriterCode(builder, type, properties, any);
-      return compileCodecClass(generatedPackage, className, code);
+      return compileObjectCodecClass(type, generatedPackage, className, code);
     }
     Function<int[], String> source =
         groupEnds -> {
@@ -237,7 +241,7 @@ public final class JsonCodegen {
               .genWriterCode(builder, type, properties, groupEnds);
         };
     return compileWriterClass(
-        generatedPackage, className, properties, "writeString", "writeStringMembers", source);
+        type, generatedPackage, className, properties, "writeString", "writeStringMembers", source);
   }
 
   private Class<?> buildUtf8Writer(ObjectCodec<?> codec, JsonTypeResolver resolver) {
@@ -251,7 +255,7 @@ public final class JsonCodegen {
       String code =
           new Utf8WriterCodegen(this, resolver, false)
               .genUnwrappedWriterCode(builder, type, codec, unwrapped);
-      return compileCodecClass(generatedPackage, className, code);
+      return compileObjectCodecClass(type, generatedPackage, className, code);
     }
     AnyInfo any = codec.anyInfo();
     JsonFieldInfo[] properties = codec.writeFields();
@@ -261,7 +265,7 @@ public final class JsonCodegen {
       String code =
           new Utf8WriterCodegen(this, resolver, false)
               .genAnyWriterCode(builder, type, properties, any);
-      return compileCodecClass(generatedPackage, className, code);
+      return compileObjectCodecClass(type, generatedPackage, className, code);
     }
     Function<int[], String> normalSource =
         groupEnds -> {
@@ -293,11 +297,11 @@ public final class JsonCodegen {
       int expandedSize =
           methodSize(codeStats(generatedPackage, className, expandedSource), "writeUtf8");
       if (expandedSize > HOT_INLINE_LIMIT) {
-        return compileCodecClass(generatedPackage, className, expandedSource);
+        return compileObjectCodecClass(type, generatedPackage, className, expandedSource);
       }
     }
     return compileUtf8WriterClass(
-        generatedPackage, className, properties, "writeUtf8", directSource, groupedSource);
+        type, generatedPackage, className, properties, "writeUtf8", directSource, groupedSource);
   }
 
   private Class<?> buildLatin1Reader(ObjectCodec<?> codec, JsonTypeResolver resolver) {
@@ -311,7 +315,7 @@ public final class JsonCodegen {
       String code =
           new Latin1ReaderCodegen(this, resolver)
               .genUnwrappedReaderCode(builder, type, codec, unwrapped);
-      return compileCodecClass(generatedPackage, className, code);
+      return compileObjectCodecClass(type, generatedPackage, className, code);
     }
     AnyInfo any = codec.anyInfo();
     JsonFieldInfo[] properties = codec.readFields();
@@ -325,6 +329,7 @@ public final class JsonCodegen {
               : reader.genAnyReaderCode(builder, type, properties, codec.creatorInfo(), any);
         };
     return compileReaderClass(
+        type,
         generatedPackage,
         className,
         properties.length,
@@ -344,7 +349,7 @@ public final class JsonCodegen {
       String code =
           new Utf16ReaderCodegen(this, resolver)
               .genUnwrappedReaderCode(builder, type, codec, unwrapped);
-      return compileCodecClass(generatedPackage, className, code);
+      return compileObjectCodecClass(type, generatedPackage, className, code);
     }
     AnyInfo any = codec.anyInfo();
     JsonFieldInfo[] properties = codec.readFields();
@@ -358,6 +363,7 @@ public final class JsonCodegen {
               : reader.genAnyReaderCode(builder, type, properties, codec.creatorInfo(), any);
         };
     return compileReaderClass(
+        type,
         generatedPackage,
         className,
         properties.length,
@@ -378,7 +384,7 @@ public final class JsonCodegen {
       String code =
           new Utf8ReaderCodegen(this, resolver, finalDependencies)
               .genUnwrappedReaderCode(builder, type, codec, unwrapped);
-      return compileCodecClass(generatedPackage, className, code);
+      return compileObjectCodecClass(type, generatedPackage, className, code);
     }
     AnyInfo any = codec.anyInfo();
     JsonFieldInfo[] properties = codec.readFields();
@@ -393,6 +399,7 @@ public final class JsonCodegen {
               : reader.genAnyReaderCode(builder, type, properties, codec.creatorInfo(), any);
         };
     return compileReaderClass(
+        type,
         generatedPackage,
         className,
         properties.length,
@@ -402,6 +409,7 @@ public final class JsonCodegen {
   }
 
   private Class<?> compileReaderClass(
+      Class<?> ownerType,
       String generatedPackage,
       String className,
       int propertyCount,
@@ -412,10 +420,11 @@ public final class JsonCodegen {
         groupable
             ? readerGroupEnds(generatedPackage, className, propertyCount, readMethod, source)
             : oneGroup(propertyCount);
-    return compileCodecClass(generatedPackage, className, source.apply(groupEnds));
+    return compileObjectCodecClass(ownerType, generatedPackage, className, source.apply(groupEnds));
   }
 
   private Class<?> compileWriterClass(
+      Class<?> ownerType,
       String generatedPackage,
       String className,
       JsonFieldInfo[] properties,
@@ -423,7 +432,7 @@ public final class JsonCodegen {
       String memberMethod,
       Function<int[], String> source) {
     if (properties.length < 2) {
-      return compileCodecClass(generatedPackage, className, source.apply(null));
+      return compileObjectCodecClass(ownerType, generatedPackage, className, source.apply(null));
     }
     // Group only the bytecode emitted in this generated class. A callee with its own stable
     // boundary contributes its invocation, not the body that C2 must keep in the callee.
@@ -431,7 +440,7 @@ public final class JsonCodegen {
     JaninoUtils.CodeStats oneGroupStats =
         codeStats(generatedPackage, className, source.apply(oneGroup));
     if (privateMethodSize(oneGroupStats, writeMethod + "Object") <= HOT_INLINE_LIMIT) {
-      return compileCodecClass(generatedPackage, className, source.apply(null));
+      return compileObjectCodecClass(ownerType, generatedPackage, className, source.apply(null));
     }
     int[] groupEnds =
         writerGroupEnds(
@@ -442,10 +451,11 @@ public final class JsonCodegen {
             writeMethod,
             memberMethod,
             source);
-    return compileCodecClass(generatedPackage, className, source.apply(groupEnds));
+    return compileObjectCodecClass(ownerType, generatedPackage, className, source.apply(groupEnds));
   }
 
   private Class<?> compileUtf8WriterClass(
+      Class<?> ownerType,
       String generatedPackage,
       String className,
       JsonFieldInfo[] properties,
@@ -455,19 +465,19 @@ public final class JsonCodegen {
     if (properties.length < 2
         || methodSize(codeStats(generatedPackage, className, directSource), writeMethod)
             <= HOT_INLINE_LIMIT) {
-      return compileCodecClass(generatedPackage, className, directSource);
+      return compileObjectCodecClass(ownerType, generatedPackage, className, directSource);
     }
     int firstGroupMember = JsonWriterCodegen.firstGroupMember(properties);
     if (properties.length - firstGroupMember < 2) {
-      return compileCodecClass(generatedPackage, className, directSource);
+      return compileObjectCodecClass(ownerType, generatedPackage, className, directSource);
     }
     int[] groupEnds =
         utf8WriterGroupEnds(
             generatedPackage, className, properties.length, firstGroupMember, writeMethod, source);
     if (groupEnds.length < 2) {
-      return compileCodecClass(generatedPackage, className, directSource);
+      return compileObjectCodecClass(ownerType, generatedPackage, className, directSource);
     }
-    return compileCodecClass(generatedPackage, className, source.apply(groupEnds));
+    return compileObjectCodecClass(ownerType, generatedPackage, className, source.apply(groupEnds));
   }
 
   private int[] utf8WriterGroupEnds(
@@ -645,6 +655,22 @@ public final class JsonCodegen {
     return result;
   }
 
+  private Class<?> compileObjectCodecClass(
+      Class<?> ownerType, String generatedPackage, String className, String code) {
+    if (!hostedCodegen || _JDKAccess.isExported(ownerType)) {
+      return compileCodecClass(generatedPackage, className, code);
+    }
+    try {
+      // A codec for a concealed model package must live beside the model to access its public
+      // members without an application export or open. Exported and bootstrap models stay in the
+      // generated loader, which also avoids changing their module graph.
+      CompileUnit unit = new CompileUnit(generatedPackage, className, code);
+      return compileHostedClass(ownerType, unit);
+    } catch (Throwable e) {
+      throw new ForyJsonException("Cannot compile generated JSON codec " + className, e);
+    }
+  }
+
   private Class<?> compileCodecClass(String generatedPackage, String className, String code) {
     try {
       CompileUnit unit = new CompileUnit(generatedPackage, className, code);
@@ -655,6 +681,38 @@ public final class JsonCodegen {
     }
   }
 
+  private Class<?> compileHostedClass(Class<?> ownerType, CompileUnit unit) {
+    Map<String, byte[]> classes = JaninoUtils.toBytecode(jsonLoader, "", unit);
+    String mainClassName = unit.getQualifiedClassName();
+    String mainClassPath = mainClassName.replace('.', '/') + ".class";
+    byte[] mainBytecode = classes.get(mainClassPath);
+    if (mainBytecode == null) {
+      throw new ForyJsonException("Missing generated JSON codec bytecode " + mainClassName);
+    }
+    ClassLoader ownerLoader = ownerType.getClassLoader();
+    if (ownerLoader == null) {
+      throw new ForyJsonException(
+          "Cannot define generated JSON codec beside bootstrap type " + ownerType.getName());
+    }
+    Object ownerModule = _JDKAccess.getModule(ownerType);
+    // The generated source names APIs from both JSON and core. A concealed third-party model
+    // package may not already read either module, so establish only those two implementation
+    // dependencies before defining the ordinary class in the model module.
+    _JDKAccess.addReads(ownerModule, _JDKAccess.getModule(JsonCodegen.class));
+    _JDKAccess.addReads(ownerModule, _JDKAccess.getModule(DefineClass.class));
+    Class<?> mainClass =
+        DefineClass.defineClass(
+            mainClassName, ownerType, ownerLoader, ownerType.getProtectionDomain(), mainBytecode);
+    for (Map.Entry<String, byte[]> entry : classes.entrySet()) {
+      if (!entry.getKey().equals(mainClassPath)) {
+        String className = CodeGenerator.fullClassNameFromClassFilePath(entry.getKey());
+        DefineClass.defineClass(
+            className, ownerType, ownerLoader, ownerType.getProtectionDomain(), entry.getValue());
+      }
+    }
+    return mainClass;
+  }
+
   @Internal
   public boolean canCompileWriter(ObjectCodec<?> codec) {
     if (!canCompileType(codec.type())) {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegenKey.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegenKey.java
new file mode 100644
index 000000000..a94494944
--- /dev/null
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegenKey.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.json.codegen;
+
+import java.util.Objects;
+import org.apache.fory.annotation.Internal;
+import org.apache.fory.json.PropertyNamingStrategy;
+
+/** Immutable identity for settings which can change generated Fory JSON source. */
+@Internal
+public final class JsonCodegenKey {
+  private final boolean writeNullFields;
+  private final boolean propertyDiscoveryEnabled;
+  private final String propertyNamingStrategy;
+  private final String codecRegistryKey;
+  private final String mixinKey;
+
+  public JsonCodegenKey(
+      boolean writeNullFields,
+      boolean propertyDiscoveryEnabled,
+      PropertyNamingStrategy propertyNamingStrategy,
+      String codecRegistryKey,
+      String mixinKey) {
+    this.writeNullFields = writeNullFields;
+    this.propertyDiscoveryEnabled = propertyDiscoveryEnabled;
+    this.propertyNamingStrategy = propertyNamingStrategy.name();
+    this.codecRegistryKey = codecRegistryKey;
+    this.mixinKey = mixinKey;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (!(other instanceof JsonCodegenKey)) {
+      return false;
+    }
+    JsonCodegenKey that = (JsonCodegenKey) other;
+    return writeNullFields == that.writeNullFields
+        && propertyDiscoveryEnabled == that.propertyDiscoveryEnabled
+        && propertyNamingStrategy.equals(that.propertyNamingStrategy)
+        && codecRegistryKey.equals(that.codecRegistryKey)
+        && mixinKey.equals(that.mixinKey);
+  }
+
+  @Override
+  public int hashCode() {
+    int result =
+        Objects.hash(
+            writeNullFields, propertyDiscoveryEnabled, propertyNamingStrategy, codecRegistryKey);
+    return 31 * result + mixinKey.hashCode();
+  }
+}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java
index 81a9c2e5e..5a7e564c3 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java
@@ -26,11 +26,15 @@ import java.lang.reflect.Executable;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.Arrays;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import org.apache.fory.annotation.Internal;
+import org.apache.fory.collection.ClassValueCache;
 import org.apache.fory.json.ForyJsonException;
 import org.apache.fory.json.codec.GeneratedJsonCodec;
 import org.apache.fory.json.resolver.JsonTypeResolver;
 import org.apache.fory.platform.AndroidSupport;
+import org.apache.fory.platform.GraalvmSupport;
 import org.apache.fory.platform.internal._JDKAccess;
 
 /**
@@ -44,6 +48,11 @@ import org.apache.fory.platform.internal._JDKAccess;
  */
 @Internal
 public final class JsonCreatorInfo {
+  // The Feature resolves each discovered executable through creatorHandle during analysis, and
+  // runtime configurations retrieve the retained handles through the same cache.
+  private static final ClassValueCache<ConcurrentMap<Executable, CreatorHandles>> NATIVE_CREATORS =
+      ClassValueCache.newClassKeyCache(32);
+
   private final Class<?> ownerType;
   private final Executable executable;
   private final JsonCreatorFieldInfo[] fields;
@@ -63,10 +72,7 @@ public final class JsonCreatorInfo {
     this.fields = fields;
     this.defaults = defaults;
     this.generatedCodec = generatedCodec;
-    invoker =
-        generatedCodec == null
-            ? buildInvoker(ownerType, executable, executable.getParameterCount())
-            : null;
+    invoker = generatedCodec == null ? buildInvoker(executable) : null;
     hashes = new long[fields.length];
     for (int i = 0; i < fields.length; i++) {
       hashes[i] = fields[i].nameHash();
@@ -117,12 +123,10 @@ public final class JsonCreatorInfo {
       return invoke(arguments);
     }
     try {
-      Object value;
-      if (executable instanceof Constructor) {
-        value = ((Constructor<?>) executable).newInstance(arguments);
-      } else {
-        value = ((Method) executable).invoke(null, arguments);
-      }
+      Object value =
+          executable instanceof Constructor
+              ? ((Constructor<?>) executable).newInstance(arguments)
+              : ((Method) executable).invoke(null, arguments);
       return requireResult(value);
     } catch (InstantiationException | IllegalAccessException e) {
       throw new ForyJsonException("Failed to invoke JSON creator for " + ownerType.getName(), e);
@@ -156,27 +160,78 @@ public final class JsonCreatorInfo {
     return value;
   }
 
-  private static MethodHandle buildInvoker(
-      Class<?> ownerType, Executable executable, int parameterCount) {
+  private static MethodHandle buildInvoker(Executable executable) {
     if (AndroidSupport.IS_ANDROID) {
       // Android has no supported trusted MethodHandle lookup. Creator shape validation guarantees
       // a public executable; accessibility is needed only when its declaring class is non-public.
       executable.setAccessible(true);
       return null;
     }
+    return creatorHandle(executable);
+  }
+
+  /** Returns the array-argument invocation handle for one JSON creator. */
+  @Internal
+  public static MethodHandle creatorHandle(Executable executable) {
+    if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+      return nativeCreatorHandles(executable).arrayInvoker;
+    }
+    return arrayInvoker(creatorTarget(executable), executable.getParameterCount());
+  }
+
+  private static MethodHandle arrayInvoker(MethodHandle target, int parameterCount) {
+    // The interpreted reader already owns one trusted fixed-size argument array. Spread that
+    // exact array into the creator without a second carrier or per-call reflective access check.
+    return target
+        .asSpreader(Object[].class, parameterCount)
+        .asType(MethodType.methodType(Object.class, Object[].class));
+  }
+
+  /** Returns the one-String-argument creator used by a JsonValue representation. */
+  @Internal
+  public static MethodHandle stringCreatorHandle(Executable executable) {
+    if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+      return nativeCreatorHandles(executable).stringInvoker;
+    }
+    return creatorTarget(executable).asType(MethodType.methodType(Object.class, String.class));
+  }
+
+  private static CreatorHandles nativeCreatorHandles(Executable executable) {
+    ConcurrentMap<Executable, CreatorHandles> creators =
+        NATIVE_CREATORS.get(executable.getDeclaringClass(), ConcurrentHashMap::new);
+    return creators.computeIfAbsent(executable, JsonCreatorInfo::newCreatorHandles);
+  }
+
+  private static CreatorHandles newCreatorHandles(Executable executable) {
+    MethodHandle target = creatorTarget(executable);
+    MethodHandle stringInvoker =
+        executable.getParameterCount() == 1 && executable.getParameterTypes()[0] == String.class
+            ? target.asType(MethodType.methodType(Object.class, String.class))
+            : null;
+    return new CreatorHandles(arrayInvoker(target, executable.getParameterCount()), stringInvoker);
+  }
+
+  private static MethodHandle creatorTarget(Executable executable) {
+    Class<?> declaringClass = executable.getDeclaringClass();
     try {
-      MethodHandle target =
-          executable instanceof Constructor
-              ? _JDKAccess._trustedLookup(ownerType)
-                  .unreflectConstructor((Constructor<?>) executable)
-              : _JDKAccess._trustedLookup(ownerType).unreflect((Method) executable);
-      // The interpreted reader already owns one trusted fixed-size argument array. Spread that
-      // exact array into the creator without a second carrier or per-call reflective access check.
-      return target
-          .asSpreader(Object[].class, parameterCount)
-          .asType(MethodType.methodType(Object.class, Object[].class));
+      // A target-class trusted lookup has full member access without requiring the application
+      // module to export or open its model package.
+      return executable instanceof Constructor
+          ? _JDKAccess._trustedLookup(declaringClass)
+              .unreflectConstructor((Constructor<?>) executable)
+          : _JDKAccess._trustedLookup(declaringClass).unreflect((Method) executable);
     } catch (IllegalAccessException e) {
-      throw new ForyJsonException("Cannot access JSON creator for " + ownerType.getName(), e);
+      throw new ForyJsonException("Cannot access JSON creator " + executable, e);
+    }
+  }
+
+  private static final class CreatorHandles {
+    private final MethodHandle arrayInvoker;
+    private final MethodHandle stringInvoker;
+
+    private CreatorHandles(MethodHandle arrayInvoker, MethodHandle stringInvoker) {
+      this.arrayInvoker = arrayInvoker;
+      this.stringInvoker = stringInvoker;
     }
   }
 }
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java
index a6cd06ee1..b4f3ad7fe 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java
@@ -22,9 +22,14 @@ package org.apache.fory.json.meta;
 import java.lang.invoke.MethodHandle;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Member;
 import java.lang.reflect.Method;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import org.apache.fory.collection.ClassValueCache;
 import org.apache.fory.json.ForyJsonException;
 import org.apache.fory.platform.AndroidSupport;
+import org.apache.fory.platform.GraalvmSupport;
 import org.apache.fory.platform.internal._JDKAccess;
 import org.apache.fory.reflect.FieldAccessor;
 
@@ -37,6 +42,11 @@ import org.apache.fory.reflect.FieldAccessor;
  * generated codecs consume the original field or method metadata and emit direct expressions.
  */
 public abstract class JsonFieldAccessor {
+  // The Feature calls the ordinary factories during analysis, so Native Image retains the same
+  // accessor instances later returned while runtime configurations build interpreted codecs.
+  private static final ClassValueCache<ConcurrentMap<Member, JsonFieldAccessor>> NATIVE_ACCESSORS =
+      ClassValueCache.newClassKeyCache(32);
+
   public Object getObject(Object target) {
     throw new UnsupportedOperationException();
   }
@@ -126,15 +136,40 @@ public abstract class JsonFieldAccessor {
   }
 
   public static JsonFieldAccessor forField(Field field) {
-    return new FieldJsonAccessor(FieldAccessor.createAccessor(field));
+    if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+      return nativeAccessors(field).computeIfAbsent(field, JsonFieldAccessor::newFieldAccessor);
+    }
+    return newFieldAccessor(field);
   }
 
   public static JsonFieldAccessor forGetter(Method getter) {
-    return new GetterJsonAccessor(getter);
+    if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+      return nativeAccessors(getter).computeIfAbsent(getter, JsonFieldAccessor::newGetterAccessor);
+    }
+    return newGetterAccessor(getter);
   }
 
   public static JsonFieldAccessor forSetter(Method setter) {
-    return new SetterJsonAccessor(setter);
+    if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+      return nativeAccessors(setter).computeIfAbsent(setter, JsonFieldAccessor::newSetterAccessor);
+    }
+    return newSetterAccessor(setter);
+  }
+
+  private static ConcurrentMap<Member, JsonFieldAccessor> nativeAccessors(Member member) {
+    return NATIVE_ACCESSORS.get(member.getDeclaringClass(), ConcurrentHashMap::new);
+  }
+
+  private static JsonFieldAccessor newFieldAccessor(Member member) {
+    return new FieldJsonAccessor(FieldAccessor.createAccessor((Field) member));
+  }
+
+  private static JsonFieldAccessor newGetterAccessor(Member member) {
+    return new GetterJsonAccessor((Method) member);
+  }
+
+  private static JsonFieldAccessor newSetterAccessor(Member member) {
+    return new SetterJsonAccessor((Method) member);
   }
 
   private static final class FieldJsonAccessor extends JsonFieldAccessor {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java
index 53ac2cd30..f6bc4539c 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java
@@ -49,9 +49,9 @@ import org.apache.fory.json.resolver.JsonTypeResolver;
  * Representation-neutral JSON cursor and common scalar parsing owner.
  *
  * <p>The base class retains the resolver used by dynamic codecs, the current code-unit position,
- * configured and current container depth, a reusable ASCII token view, and a fixed workspace for
- * exact floating-point boundary correction. Concrete readers own input storage, string decoding,
- * field-name probes, and direct primitive numeric fast paths for their representation.
+ * configured and current container depth, reusable creator and numeric workspaces, and an ASCII
+ * token view. Concrete readers own input storage, string decoding, field-name probes, and direct
+ * primitive numeric fast paths for their representation.
  *
  * <p>Primitive {@code int}, {@code long}, {@code float}, and {@code double} parsing does not
  * materialize a String or arbitrary-precision number. Precision-sensitive floating input uses the
@@ -256,6 +256,7 @@ public abstract class JsonReader {
   public abstract int readSubtypeName(JsonSubtypeScanInfo info);
 
   private final AsciiStringView asciiStringView = new AsciiStringView(this);
+  private final Object[] creatorArguments = new Object[1];
   // Primitive floating fallback reuses this exact-boundary workspace. Reader construction is the
   // cold owner so the first precision-sensitive scalar cannot allocate on the numeric hot path.
   private final byte[] decimalBoundaryDigits = new byte[DECIMAL_BOUNDARY_DIGITS];
@@ -272,6 +273,19 @@ public abstract class JsonReader {
     return typeResolver;
   }
 
+  /** Returns this reader's reusable one-value creator argument array. */
+  @Internal
+  public final Object[] creatorArguments(Object value) {
+    creatorArguments[0] = value;
+    return creatorArguments;
+  }
+
+  /** Releases the value retained by {@link #creatorArguments(Object)}. */
+  @Internal
+  public final void clearCreatorArguments() {
+    creatorArguments[0] = null;
+  }
+
   protected abstract int length();
 
   protected abstract char charAt(int index);
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java
index 14773aaed..e9088a547 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java
@@ -32,10 +32,10 @@ import org.apache.fory.util.Preconditions;
  * Builder-side registry of exact user-supplied {@link JsonValueCodec} bindings.
  *
  * <p>Registration is keyed by class identity and replaces any previous codec for the exact class. A
- * {@link JsonSharedRegistry} receives a copy when a runtime is built, separating later builder
- * mutation from an existing {@code ForyJson}. The deterministic {@link #codegenKey()} describes
- * codec classes that can affect generated source without retaining codec instances in process-wide
- * code-generation naming state.
+ * {@code JsonConfig} receives a copy when a runtime is built, separating later builder mutation
+ * from an existing {@code ForyJson}. The runtime registry reads that owned snapshot directly. The
+ * deterministic {@link #codegenKey()} describes codec classes that can affect generated source
+ * without retaining codec instances in process-wide code-generation naming state.
  */
 public final class CodecRegistry {
   private final ConcurrentMap<Class<?>, JsonValueCodec<?>> codecs;
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java
index 4e382b3bd..06edaa87f 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java
@@ -32,12 +32,23 @@ import org.apache.fory.json.codec.Utf8WriterCodec;
 import org.apache.fory.json.meta.JsonFieldInfo;
 import org.apache.fory.json.meta.JsonFieldTable;
 import org.apache.fory.platform.AndroidSupport;
+import org.apache.fory.platform.GraalvmSupport;
 import org.apache.fory.platform.internal._JDKAccess;
+import org.apache.fory.reflect.ReflectionUtils;
 
 /** Invokes generated codec constructor contracts selected and owned by {@link JsonTypeResolver}. */
 final class GeneratedCodecInstantiator {
   private GeneratedCodecInstantiator() {}
 
+  private static MethodHandle constructor(Class<?> type, Class<?>... parameterTypes)
+      throws NoSuchMethodException, IllegalAccessException {
+    if (GraalvmSupport.isGraalRuntime()) {
+      return ReflectionUtils.getCtrHandle(type, parameterTypes);
+    }
+    return _JDKAccess._trustedLookup(type)
+        .findConstructor(type, MethodType.methodType(void.class, parameterTypes));
+  }
+
   @SuppressWarnings("unchecked")
   static StringWriterCodec<Object> instantiateStringWriter(
       Class<?> type, JsonFieldInfo[] fields, StringWriterCodec<Object>[] codecs) {
@@ -49,11 +60,7 @@ final class GeneratedCodecInstantiator {
         return (StringWriterCodec<Object>) constructor.newInstance(fields, codecs);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class, JsonFieldInfo[].class, StringWriterCodec[].class));
+          constructor(type, JsonFieldInfo[].class, StringWriterCodec[].class);
       return (StringWriterCodec<Object>) constructor.invoke(fields, codecs);
     } catch (Throwable e) {
       throw new ForyJsonException("Cannot instantiate generated JSON String writer", e);
@@ -75,14 +82,7 @@ final class GeneratedCodecInstantiator {
         return (StringWriterCodec<Object>) constructor.newInstance(owner, fields, codecs);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class,
-                      ObjectCodec.class,
-                      JsonFieldInfo[].class,
-                      StringWriterCodec[].class));
+          constructor(type, ObjectCodec.class, JsonFieldInfo[].class, StringWriterCodec[].class);
       return (StringWriterCodec<Object>) constructor.invoke(owner, fields, codecs);
     } catch (Throwable e) {
       throw new ForyJsonException("Cannot instantiate generated JSON Any String writer", e);
@@ -108,15 +108,12 @@ final class GeneratedCodecInstantiator {
         return (StringWriterCodec<Object>) constructor.newInstance(owner, fields, codecs, anyCodec);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class,
-                      ObjectCodec.class,
-                      JsonFieldInfo[].class,
-                      StringWriterCodec[].class,
-                      StringWriterCodec.class));
+          constructor(
+              type,
+              ObjectCodec.class,
+              JsonFieldInfo[].class,
+              StringWriterCodec[].class,
+              StringWriterCodec.class);
       return (StringWriterCodec<Object>) constructor.invoke(owner, fields, codecs, anyCodec);
     } catch (Throwable e) {
       throw new ForyJsonException("Cannot instantiate generated JSON Any String writer", e);
@@ -133,12 +130,7 @@ final class GeneratedCodecInstantiator {
         constructor.setAccessible(true);
         return (Utf8WriterCodec<Object>) constructor.newInstance(fields, codecs);
       }
-      MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class, JsonFieldInfo[].class, Utf8WriterCodec[].class));
+      MethodHandle constructor = constructor(type, JsonFieldInfo[].class, Utf8WriterCodec[].class);
       return (Utf8WriterCodec<Object>) constructor.invoke(fields, codecs);
     } catch (Throwable e) {
       throw new ForyJsonException("Cannot instantiate generated JSON UTF8 writer", e);
@@ -160,14 +152,7 @@ final class GeneratedCodecInstantiator {
         return (Utf8WriterCodec<Object>) constructor.newInstance(owner, fields, codecs);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class,
-                      ObjectCodec.class,
-                      JsonFieldInfo[].class,
-                      Utf8WriterCodec[].class));
+          constructor(type, ObjectCodec.class, JsonFieldInfo[].class, Utf8WriterCodec[].class);
       return (Utf8WriterCodec<Object>) constructor.invoke(owner, fields, codecs);
     } catch (Throwable e) {
       throw new ForyJsonException("Cannot instantiate generated JSON Any UTF8 writer", e);
@@ -193,15 +178,12 @@ final class GeneratedCodecInstantiator {
         return (Utf8WriterCodec<Object>) constructor.newInstance(owner, fields, codecs, anyCodec);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class,
-                      ObjectCodec.class,
-                      JsonFieldInfo[].class,
-                      Utf8WriterCodec[].class,
-                      Utf8WriterCodec.class));
+          constructor(
+              type,
+              ObjectCodec.class,
+              JsonFieldInfo[].class,
+              Utf8WriterCodec[].class,
+              Utf8WriterCodec.class);
       return (Utf8WriterCodec<Object>) constructor.invoke(owner, fields, codecs, anyCodec);
     } catch (Throwable e) {
       throw new ForyJsonException("Cannot instantiate generated JSON Any UTF8 writer", e);
@@ -223,14 +205,7 @@ final class GeneratedCodecInstantiator {
         return (Latin1ReaderCodec<Object>) constructor.newInstance(owner, fields, codecs);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class,
-                      ObjectCodec.class,
-                      JsonFieldInfo[].class,
-                      Latin1ReaderCodec[].class));
+          constructor(type, ObjectCodec.class, JsonFieldInfo[].class, Latin1ReaderCodec[].class);
       return (Latin1ReaderCodec<Object>) constructor.invoke(owner, fields, codecs);
     } catch (Throwable e) {
       throw new ForyJsonException("Cannot instantiate generated JSON Latin1 reader", e);
@@ -259,16 +234,13 @@ final class GeneratedCodecInstantiator {
             constructor.newInstance(owner, readTable, fields, codecs, selfReader);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class,
-                      ObjectCodec.class,
-                      JsonFieldTable.class,
-                      JsonFieldInfo[].class,
-                      Latin1ReaderCodec[].class,
-                      Latin1ReaderCodec.class));
+          constructor(
+              type,
+              ObjectCodec.class,
+              JsonFieldTable.class,
+              JsonFieldInfo[].class,
+              Latin1ReaderCodec[].class,
+              Latin1ReaderCodec.class);
       return (Latin1ReaderCodec<Object>)
           constructor.invoke(owner, readTable, fields, codecs, selfReader);
     } catch (Throwable e) {
@@ -300,17 +272,14 @@ final class GeneratedCodecInstantiator {
             constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class,
-                      ObjectCodec.class,
-                      JsonFieldTable.class,
-                      JsonFieldInfo[].class,
-                      Latin1ReaderCodec[].class,
-                      Latin1ReaderCodec.class,
-                      Latin1ReaderCodec.class));
+          constructor(
+              type,
+              ObjectCodec.class,
+              JsonFieldTable.class,
+              JsonFieldInfo[].class,
+              Latin1ReaderCodec[].class,
+              Latin1ReaderCodec.class,
+              Latin1ReaderCodec.class);
       return (Latin1ReaderCodec<Object>)
           constructor.invoke(owner, readTable, fields, codecs, selfReader, anyCodec);
     } catch (Throwable e) {
@@ -333,14 +302,7 @@ final class GeneratedCodecInstantiator {
         return (Utf16ReaderCodec<Object>) constructor.newInstance(owner, fields, codecs);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class,
-                      ObjectCodec.class,
-                      JsonFieldInfo[].class,
-                      Utf16ReaderCodec[].class));
+          constructor(type, ObjectCodec.class, JsonFieldInfo[].class, Utf16ReaderCodec[].class);
       return (Utf16ReaderCodec<Object>) constructor.invoke(owner, fields, codecs);
     } catch (Throwable e) {
       throw new ForyJsonException("Cannot instantiate generated JSON UTF16 reader", e);
@@ -369,16 +331,13 @@ final class GeneratedCodecInstantiator {
             constructor.newInstance(owner, readTable, fields, codecs, selfReader);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class,
-                      ObjectCodec.class,
-                      JsonFieldTable.class,
-                      JsonFieldInfo[].class,
-                      Utf16ReaderCodec[].class,
-                      Utf16ReaderCodec.class));
+          constructor(
+              type,
+              ObjectCodec.class,
+              JsonFieldTable.class,
+              JsonFieldInfo[].class,
+              Utf16ReaderCodec[].class,
+              Utf16ReaderCodec.class);
       return (Utf16ReaderCodec<Object>)
           constructor.invoke(owner, readTable, fields, codecs, selfReader);
     } catch (Throwable e) {
@@ -410,17 +369,14 @@ final class GeneratedCodecInstantiator {
             constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class,
-                      ObjectCodec.class,
-                      JsonFieldTable.class,
-                      JsonFieldInfo[].class,
-                      Utf16ReaderCodec[].class,
-                      Utf16ReaderCodec.class,
-                      Utf16ReaderCodec.class));
+          constructor(
+              type,
+              ObjectCodec.class,
+              JsonFieldTable.class,
+              JsonFieldInfo[].class,
+              Utf16ReaderCodec[].class,
+              Utf16ReaderCodec.class,
+              Utf16ReaderCodec.class);
       return (Utf16ReaderCodec<Object>)
           constructor.invoke(owner, readTable, fields, codecs, selfReader, anyCodec);
     } catch (Throwable e) {
@@ -443,14 +399,7 @@ final class GeneratedCodecInstantiator {
         return (Utf8ReaderCodec<Object>) constructor.newInstance(owner, fields, codecs);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class,
-                      ObjectCodec.class,
-                      JsonFieldInfo[].class,
-                      Utf8ReaderCodec[].class));
+          constructor(type, ObjectCodec.class, JsonFieldInfo[].class, Utf8ReaderCodec[].class);
       return (Utf8ReaderCodec<Object>) constructor.invoke(owner, fields, codecs);
     } catch (Throwable e) {
       throw new ForyJsonException("Cannot instantiate generated JSON UTF8 reader", e);
@@ -466,9 +415,7 @@ final class GeneratedCodecInstantiator {
         constructor.setAccessible(true);
         return (Utf8WriterCodec<Object>) constructor.newInstance(fallback);
       }
-      MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(type, MethodType.methodType(void.class, Utf8WriterCodec.class));
+      MethodHandle constructor = constructor(type, Utf8WriterCodec.class);
       return (Utf8WriterCodec<Object>) constructor.invoke(fallback);
     } catch (Throwable e) {
       throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection writer", e);
@@ -485,11 +432,7 @@ final class GeneratedCodecInstantiator {
         constructor.setAccessible(true);
         return (Utf8WriterCodec<Object>) constructor.newInstance(fallback, elementWriter);
       }
-      MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(void.class, Utf8WriterCodec.class, Utf8WriterCodec.class));
+      MethodHandle constructor = constructor(type, Utf8WriterCodec.class, Utf8WriterCodec.class);
       return (Utf8WriterCodec<Object>) constructor.invoke(fallback, elementWriter);
     } catch (Throwable e) {
       throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection writer", e);
@@ -505,9 +448,7 @@ final class GeneratedCodecInstantiator {
         constructor.setAccessible(true);
         return (Utf8ReaderCodec<Object>) constructor.newInstance(elementReader);
       }
-      MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(type, MethodType.methodType(void.class, Utf8ReaderCodec.class));
+      MethodHandle constructor = constructor(type, Utf8ReaderCodec.class);
       return (Utf8ReaderCodec<Object>) constructor.invoke(elementReader);
     } catch (Throwable e) {
       throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection reader", e);
@@ -536,16 +477,13 @@ final class GeneratedCodecInstantiator {
             constructor.newInstance(owner, readTable, fields, codecs, selfReader);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class,
-                      ObjectCodec.class,
-                      JsonFieldTable.class,
-                      JsonFieldInfo[].class,
-                      Utf8ReaderCodec[].class,
-                      Utf8ReaderCodec.class));
+          constructor(
+              type,
+              ObjectCodec.class,
+              JsonFieldTable.class,
+              JsonFieldInfo[].class,
+              Utf8ReaderCodec[].class,
+              Utf8ReaderCodec.class);
       return (Utf8ReaderCodec<Object>)
           constructor.invoke(owner, readTable, fields, codecs, selfReader);
     } catch (Throwable e) {
@@ -577,17 +515,14 @@ final class GeneratedCodecInstantiator {
             constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec);
       }
       MethodHandle constructor =
-          _JDKAccess._trustedLookup(type)
-              .findConstructor(
-                  type,
-                  MethodType.methodType(
-                      void.class,
-                      ObjectCodec.class,
-                      JsonFieldTable.class,
-                      JsonFieldInfo[].class,
-                      Utf8ReaderCodec[].class,
-                      Utf8ReaderCodec.class,
-                      Utf8ReaderCodec.class));
+          constructor(
+              type,
+              ObjectCodec.class,
+              JsonFieldTable.class,
+              JsonFieldInfo[].class,
+              Utf8ReaderCodec[].class,
+              Utf8ReaderCodec.class,
+              Utf8ReaderCodec.class);
       return (Utf8ReaderCodec<Object>)
           constructor.invoke(owner, readTable, fields, codecs, selfReader, anyCodec);
     } catch (Throwable e) {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedJsonCodecFactories.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedJsonCodecFactories.java
deleted file mode 100644
index f5d479d9f..000000000
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedJsonCodecFactories.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.fory.json.resolver;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import org.apache.fory.annotation.Internal;
-import org.apache.fory.json.ForyJsonException;
-import org.apache.fory.json.codec.GeneratedJsonCodecFactory;
-
-/** Frozen GraalVM runtime reconstruction table for generated JSON companions. */
-@Internal
-public final class GeneratedJsonCodecFactories {
-  private static Map<Key, GeneratedJsonCodecFactory> factories = new HashMap<>();
-  private static boolean frozen;
-
-  private GeneratedJsonCodecFactories() {}
-
-  /** Publishes one hosted-analysis factory for an exact target and optional Mixin pair. */
-  public static synchronized void register(
-      Class<?> type, Class<?> mixinType, GeneratedJsonCodecFactory factory) {
-    if (frozen) {
-      throw new ForyJsonException("Generated JSON codec factory table is already frozen");
-    }
-    Key key = new Key(type, mixinType);
-    GeneratedJsonCodecFactory previous = factories.get(key);
-    if (previous == null) {
-      factories.put(key, factory);
-    } else if (previous.getClass() != factory.getClass()) {
-      throw new ForyJsonException(
-          "Conflicting generated JSON codec factories for "
-              + type.getName()
-              + (mixinType == null ? "" : " and " + mixinType.getName())
-              + ": "
-              + previous.getClass().getName()
-              + " and "
-              + factory.getClass().getName());
-    }
-  }
-
-  /** Freezes the hosted table into the native image heap. */
-  public static synchronized void freeze() {
-    if (!frozen) {
-      factories = Collections.unmodifiableMap(new HashMap<>(factories));
-      frozen = true;
-    }
-  }
-
-  /** Returns the exact pair factory, or {@code null} when no companion was generated. */
-  public static synchronized GeneratedJsonCodecFactory get(Class<?> type, Class<?> mixinType) {
-    return factories.get(new Key(type, mixinType));
-  }
-
-  private static final class Key {
-    private final Class<?> type;
-    private final Class<?> mixinType;
-
-    private Key(Class<?> type, Class<?> mixinType) {
-      this.type = type;
-      this.mixinType = mixinType;
-    }
-
-    @Override
-    public boolean equals(Object other) {
-      if (this == other) {
-        return true;
-      }
-      if (other == null || getClass() != other.getClass()) {
-        return false;
-      }
-      Key that = (Key) other;
-      return type == that.type && mixinType == that.mixinType;
-    }
-
-    @Override
-    public int hashCode() {
-      return 31 * System.identityHashCode(type) + System.identityHashCode(mixinType);
-    }
-  }
-}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java
new file mode 100644
index 000000000..70200ef94
--- /dev/null
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.json.resolver;
+
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import org.apache.fory.annotation.Internal;
+import org.apache.fory.json.codegen.JsonCodegenKey;
+import org.apache.fory.json.resolver.JsonSharedRegistry.GeneratedClasses;
+
+/** Frozen Native Image mapping from JSON configuration semantics to generated classes. */
+@Internal
+public final class JsonGeneratedClassRegistry {
+  private static Map<JsonCodegenKey, MutableConfiguration> pending = new HashMap<>();
+  private static Map<JsonCodegenKey, Configuration> configurations = Collections.emptyMap();
+  private static boolean frozen;
+
+  private JsonGeneratedClassRegistry() {}
+
+  /** Publishes one hosted configuration's generated classes during Native Image analysis. */
+  public static synchronized Set<Class<?>> register(
+      JsonCodegenKey key, JsonSharedRegistry hostedRegistry) {
+    if (frozen) {
+      throw new IllegalStateException("Fory JSON generated class registry is frozen");
+    }
+    GeneratedClasses generatedClasses = hostedRegistry.generatedClasses();
+    MutableConfiguration configuration = pending.get(key);
+    if (configuration == null) {
+      configuration = new MutableConfiguration();
+      pending.put(key, configuration);
+    }
+    LinkedHashSet<Class<?>> added = new LinkedHashSet<>();
+    configuration.merge(generatedClasses, added);
+    configurations = snapshot();
+    return added;
+  }
+
+  /** Finalizes generated class lookup after Native Image analysis. */
+  public static synchronized void freeze() {
+    if (frozen) {
+      return;
+    }
+    pending = null;
+    frozen = true;
+  }
+
+  private static Map<JsonCodegenKey, Configuration> snapshot() {
+    Map<JsonCodegenKey, Configuration> snapshot = new HashMap<>(pending.size());
+    for (Map.Entry<JsonCodegenKey, MutableConfiguration> entry : pending.entrySet()) {
+      snapshot.put(entry.getKey(), entry.getValue().freeze());
+    }
+    return Collections.unmodifiableMap(snapshot);
+  }
+
+  static Configuration configuration(JsonCodegenKey key) {
+    return configurations.get(key);
+  }
+
+  static final class Configuration {
+    private final Map<Class<?>, Class<?>> stringWriters;
+    private final Map<Class<?>, Class<?>> utf8Writers;
+    private final Map<Class<?>, Class<?>> latin1Readers;
+    private final Map<Class<?>, Class<?>> utf16Readers;
+    private final Map<Class<?>, Class<?>> utf8Readers;
+    private final Map<Type, Class<?>> utf8CollectionWriters;
+    private final Map<Type, Class<?>> utf8CollectionReaders;
+
+    private Configuration(MutableConfiguration source) {
+      stringWriters = immutable(source.stringWriters);
+      utf8Writers = immutable(source.utf8Writers);
+      latin1Readers = immutable(source.latin1Readers);
+      utf16Readers = immutable(source.utf16Readers);
+      utf8Readers = immutable(source.utf8Readers);
+      utf8CollectionWriters = immutable(source.utf8CollectionWriters);
+      utf8CollectionReaders = immutable(source.utf8CollectionReaders);
+    }
+
+    Class<?> stringWriter(Class<?> type) {
+      return stringWriters.get(type);
+    }
+
+    Class<?> utf8Writer(Class<?> type) {
+      return utf8Writers.get(type);
+    }
+
+    Class<?> latin1Reader(Class<?> type) {
+      return latin1Readers.get(type);
+    }
+
+    Class<?> utf16Reader(Class<?> type) {
+      return utf16Readers.get(type);
+    }
+
+    Class<?> utf8Reader(Class<?> type) {
+      return utf8Readers.get(type);
+    }
+
+    Class<?> utf8CollectionWriter(Type type) {
+      return utf8CollectionWriters.get(type);
+    }
+
+    Class<?> utf8CollectionReader(Type type) {
+      return utf8CollectionReaders.get(type);
+    }
+
+    private static <K> Map<K, Class<?>> immutable(Map<K, Class<?>> classes) {
+      return classes.isEmpty()
+          ? Collections.emptyMap()
+          : Collections.unmodifiableMap(new HashMap<>(classes));
+    }
+  }
+
+  private static final class MutableConfiguration {
+    private final Map<Class<?>, Class<?>> stringWriters = new HashMap<>();
+    private final Map<Class<?>, Class<?>> utf8Writers = new HashMap<>();
+    private final Map<Class<?>, Class<?>> latin1Readers = new HashMap<>();
+    private final Map<Class<?>, Class<?>> utf16Readers = new HashMap<>();
+    private final Map<Class<?>, Class<?>> utf8Readers = new HashMap<>();
+    private final Map<Type, Class<?>> utf8CollectionWriters = new HashMap<>();
+    private final Map<Type, Class<?>> utf8CollectionReaders = new HashMap<>();
+
+    private void merge(GeneratedClasses source, Set<Class<?>> added) {
+      merge(source.stringWriters(), stringWriters, added);
+      merge(source.utf8Writers(), utf8Writers, added);
+      merge(source.latin1Readers(), latin1Readers, added);
+      merge(source.utf16Readers(), utf16Readers, added);
+      merge(source.utf8Readers(), utf8Readers, added);
+      merge(source.utf8CollectionWriters(), utf8CollectionWriters, added);
+      merge(source.utf8CollectionReaders(), utf8CollectionReaders, added);
+    }
+
+    private static <K> void merge(
+        Map<K, Class<?>> source, Map<K, Class<?>> target, Set<Class<?>> added) {
+      for (Map.Entry<K, Class<?>> entry : source.entrySet()) {
+        merge(entry.getKey(), entry.getValue(), target, added);
+      }
+    }
+
+    private static <K> void merge(
+        K key, Class<?> generatedClass, Map<K, Class<?>> target, Set<Class<?>> added) {
+      Class<?> previous = target.putIfAbsent(key, generatedClass);
+      if (previous == null) {
+        added.add(generatedClass);
+      } else if (previous != generatedClass) {
+        throw new IllegalStateException("Conflicting generated Fory JSON classes for " + key);
+      }
+    }
+
+    private Configuration freeze() {
+      return new Configuration(this);
+    }
+  }
+}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
index 2fe6f7f73..cce8f903e 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
@@ -110,7 +110,6 @@ import org.apache.fory.json.codec.ArrayCodec;
 import org.apache.fory.json.codec.CodecUtils;
 import org.apache.fory.json.codec.CollectionCodec;
 import org.apache.fory.json.codec.GeneratedJsonCodec;
-import org.apache.fory.json.codec.GeneratedJsonCodecFactory;
 import org.apache.fory.json.codec.GuavaCodecs;
 import org.apache.fory.json.codec.JsonSubTypesInfo;
 import org.apache.fory.json.codec.JsonValueCodec;
@@ -120,10 +119,12 @@ import org.apache.fory.json.codec.ObjectCodec;
 import org.apache.fory.json.codec.ScalarCodecs;
 import org.apache.fory.json.codec.SqlJsonCodecs;
 import org.apache.fory.json.codegen.JsonCodegen;
+import org.apache.fory.json.codegen.JsonCodegenKey;
 import org.apache.fory.json.codegen.JsonJITContext;
 import org.apache.fory.json.meta.JsonAnySetterAccessor;
 import org.apache.fory.json.meta.JsonFieldAccessor;
 import org.apache.fory.json.meta.JsonFieldKind;
+import org.apache.fory.json.resolver.JsonGeneratedClassRegistry.Configuration;
 import org.apache.fory.platform.AndroidSupport;
 import org.apache.fory.platform.GraalvmSupport;
 import org.apache.fory.reflect.ReflectionUtils;
@@ -172,6 +173,8 @@ public final class JsonSharedRegistry {
   private final ConcurrentHashMap<String, Boolean> typeCheckCache;
   private final Object typeCheckCacheLock;
   private final JsonCodegen codegen;
+  private final JsonCodegenKey nativeCodegenKey;
+  private final boolean hostedCodegen;
   private final boolean asyncCompilationEnabled;
   private final ExecutorService compilationService;
   private final boolean propertyDiscoveryEnabled;
@@ -202,13 +205,21 @@ public final class JsonSharedRegistry {
   private final ConcurrentHashMap<Long, CachedFieldName> cachedFieldNames;
 
   public JsonSharedRegistry(JsonConfig config) {
-    this(config, null);
+    this(config, null, false);
   }
 
   JsonSharedRegistry(JsonConfig config, ExecutorService compilationService) {
-    this.customCodecs = config.codecRegistry().copy();
-    typeChecker = config.typeChecker();
-    typeCheckContext = config.typeCheckContext();
+    this(config, compilationService, false);
+  }
+
+  private JsonSharedRegistry(
+      JsonConfig config, ExecutorService compilationService, boolean hostedCodegen) {
+    this.customCodecs = config.codecRegistry();
+    // Hosted compilation produces classes shared by configurations with the same source shape.
+    // Runtime type policy is intentionally not part of that shape and remains enforced by each
+    // runtime resolver before it installs a generated capability.
+    typeChecker = hostedCodegen ? null : config.typeChecker();
+    typeCheckContext = hostedCodegen ? null : config.typeCheckContext();
     typeCheckCache = typeChecker == null ? null : new ConcurrentHashMap<>();
     typeCheckCacheLock = typeChecker == null ? null : new Object();
     this.propertyDiscoveryEnabled = config.propertyDiscoveryEnabled();
@@ -237,12 +248,120 @@ public final class JsonSharedRegistry {
     utf8CollectionReaderClasses = new ConcurrentHashMap<>();
     cachedFieldNames = new ConcurrentHashMap<>();
     boolean codegenEnabled = config.codegenEnabled();
-    codegen = codegenEnabled ? new JsonCodegen(config.getCodegenHash(), classLoader) : null;
-    asyncCompilationEnabled = codegenEnabled && config.asyncCompilationEnabled();
+    this.hostedCodegen = hostedCodegen;
+    boolean createCompiler =
+        codegenEnabled && (hostedCodegen || !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE);
+    codegen =
+        createCompiler
+            ? new JsonCodegen(config.getCodegenHash(), classLoader, hostedCodegen)
+            : null;
+    nativeCodegenKey =
+        codegenEnabled && GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE ? config.codegenKey() : null;
+    asyncCompilationEnabled = createCompiler && !hostedCodegen && config.asyncCompilationEnabled();
     this.compilationService = compilationService;
     registerExactCodecs();
   }
 
+  /** Creates the transient synchronous compiler registry owned by Native Image hosted analysis. */
+  @Internal
+  public static JsonSharedRegistry forHostedCodegen(JsonConfig config) {
+    if (!GraalvmSupport.isGraalBuildTime()) {
+      throw new IllegalStateException("Hosted Fory JSON code generation requires image build time");
+    }
+    if (!config.codegenEnabled()) {
+      throw new IllegalArgumentException("Hosted Fory JSON configuration has codegen disabled");
+    }
+    return new JsonSharedRegistry(config, null, true);
+  }
+
+  /** Returns a complete immutable snapshot of every synchronously generated class. */
+  GeneratedClasses generatedClasses() {
+    if (codegen == null || asyncCompilationEnabled) {
+      throw new IllegalStateException("Generated class snapshots require synchronous codegen");
+    }
+    return new GeneratedClasses(
+        completedClasses(stringWriterClasses),
+        completedClasses(utf8WriterClasses),
+        completedClasses(latin1ReaderClasses),
+        completedClasses(utf16ReaderClasses),
+        completedClasses(utf8ReaderClasses),
+        completedClasses(utf8CollectionWriterClasses),
+        completedClasses(utf8CollectionReaderClasses));
+  }
+
+  private static <K> Map<K, Class<?>> completedClasses(
+      Map<K, CompletableFuture<Class<?>>> futures) {
+    Map<K, Class<?>> classes = new HashMap<>(futures.size());
+    for (Map.Entry<K, CompletableFuture<Class<?>>> entry : futures.entrySet()) {
+      CompletableFuture<Class<?>> future = entry.getValue();
+      if (!future.isDone() || future.isCompletedExceptionally()) {
+        throw new IllegalStateException(
+            "Fory JSON generated class is incomplete: " + entry.getKey());
+      }
+      Class<?> generatedClass = future.getNow(null);
+      if (generatedClass == null) {
+        throw new IllegalStateException("Fory JSON generated class is null: " + entry.getKey());
+      }
+      classes.put(entry.getKey(), generatedClass);
+    }
+    return Collections.unmodifiableMap(classes);
+  }
+
+  static final class GeneratedClasses {
+    private final Map<Class<?>, Class<?>> stringWriters;
+    private final Map<Class<?>, Class<?>> utf8Writers;
+    private final Map<Class<?>, Class<?>> latin1Readers;
+    private final Map<Class<?>, Class<?>> utf16Readers;
+    private final Map<Class<?>, Class<?>> utf8Readers;
+    private final Map<Type, Class<?>> utf8CollectionWriters;
+    private final Map<Type, Class<?>> utf8CollectionReaders;
+
+    private GeneratedClasses(
+        Map<Class<?>, Class<?>> stringWriters,
+        Map<Class<?>, Class<?>> utf8Writers,
+        Map<Class<?>, Class<?>> latin1Readers,
+        Map<Class<?>, Class<?>> utf16Readers,
+        Map<Class<?>, Class<?>> utf8Readers,
+        Map<Type, Class<?>> utf8CollectionWriters,
+        Map<Type, Class<?>> utf8CollectionReaders) {
+      this.stringWriters = stringWriters;
+      this.utf8Writers = utf8Writers;
+      this.latin1Readers = latin1Readers;
+      this.utf16Readers = utf16Readers;
+      this.utf8Readers = utf8Readers;
+      this.utf8CollectionWriters = utf8CollectionWriters;
+      this.utf8CollectionReaders = utf8CollectionReaders;
+    }
+
+    Map<Class<?>, Class<?>> stringWriters() {
+      return stringWriters;
+    }
+
+    Map<Class<?>, Class<?>> utf8Writers() {
+      return utf8Writers;
+    }
+
+    Map<Class<?>, Class<?>> latin1Readers() {
+      return latin1Readers;
+    }
+
+    Map<Class<?>, Class<?>> utf16Readers() {
+      return utf16Readers;
+    }
+
+    Map<Class<?>, Class<?>> utf8Readers() {
+      return utf8Readers;
+    }
+
+    Map<Type, Class<?>> utf8CollectionWriters() {
+      return utf8CollectionWriters;
+    }
+
+    Map<Type, Class<?>> utf8CollectionReaders() {
+      return utf8CollectionReaders;
+    }
+  }
+
   CompletableFuture<Class<?>> stringWriterClass(ObjectCodec<?> owner, JsonTypeResolver resolver) {
     return generatedClassFuture(
         stringWriterClasses, owner.type(), () -> codegen.compileStringWriter(owner, resolver));
@@ -284,6 +403,63 @@ public final class JsonSharedRegistry {
         () -> codegen.compileUtf8CollectionReader(declaredType, owner));
   }
 
+  boolean generatedCapabilitiesEnabled() {
+    return codegen != null || nativeConfiguration() != null;
+  }
+
+  boolean hostedCodegen() {
+    return hostedCodegen;
+  }
+
+  boolean missingNativeConfiguration() {
+    return nativeCodegenKey != null && nativeConfiguration() == null;
+  }
+
+  boolean nativeGeneratedClasses() {
+    return nativeCodegenKey != null && codegen == null && nativeConfiguration() != null;
+  }
+
+  Class<?> nativeStringWriterClass(Class<?> type) {
+    Configuration configuration = nativeConfiguration();
+    return configuration == null ? null : configuration.stringWriter(type);
+  }
+
+  Class<?> nativeUtf8WriterClass(Class<?> type) {
+    Configuration configuration = nativeConfiguration();
+    return configuration == null ? null : configuration.utf8Writer(type);
+  }
+
+  Class<?> nativeLatin1ReaderClass(Class<?> type) {
+    Configuration configuration = nativeConfiguration();
+    return configuration == null ? null : configuration.latin1Reader(type);
+  }
+
+  Class<?> nativeUtf16ReaderClass(Class<?> type) {
+    Configuration configuration = nativeConfiguration();
+    return configuration == null ? null : configuration.utf16Reader(type);
+  }
+
+  Class<?> nativeUtf8ReaderClass(Class<?> type) {
+    Configuration configuration = nativeConfiguration();
+    return configuration == null ? null : configuration.utf8Reader(type);
+  }
+
+  Class<?> nativeUtf8CollectionWriterClass(Type type) {
+    Configuration configuration = nativeConfiguration();
+    return configuration == null ? null : configuration.utf8CollectionWriter(type);
+  }
+
+  Class<?> nativeUtf8CollectionReaderClass(Type type) {
+    Configuration configuration = nativeConfiguration();
+    return configuration == null ? null : configuration.utf8CollectionReader(type);
+  }
+
+  private Configuration nativeConfiguration() {
+    return nativeCodegenKey == null
+        ? null
+        : JsonGeneratedClassRegistry.configuration(nativeCodegenKey);
+  }
+
   private <K> CompletableFuture<Class<?>> generatedClassFuture(
       ConcurrentHashMap<K, CompletableFuture<Class<?>>> classes,
       K key,
@@ -361,6 +537,9 @@ public final class JsonSharedRegistry {
   }
 
   GeneratedJsonCodec<?> generatedCodec(Class<?> type) {
+    if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+      return null;
+    }
     Class<?> mixinType = mixinType(type);
     boolean directGenerated = type.getDeclaredAnnotation(JsonType.class) != null;
     if (!directGenerated && mixinType == null) {
@@ -368,10 +547,7 @@ public final class JsonSharedRegistry {
     }
     try {
       GeneratedJsonCodec<?> codec = generatedCodecIfPresent(type, mixinType);
-      if (codec == null
-          && (directGenerated
-              || mixinType != null
-                  && (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE))) {
+      if (codec == null && (directGenerated || mixinType != null && AndroidSupport.IS_ANDROID)) {
         throw missingGeneratedCodec(type, mixinType, "JSON object model");
       }
       return codec;
@@ -418,10 +594,6 @@ public final class JsonSharedRegistry {
   }
 
   private GeneratedJsonCodec<?> loadGeneratedCodec(Class<?> type, Class<?> mixinType) {
-    if (GraalvmSupport.isGraalRuntime()) {
-      GeneratedJsonCodecFactory factory = GeneratedJsonCodecFactories.get(type, mixinType);
-      return factory == null ? null : validateGeneratedCodec(type, factory.create());
-    }
     String generatedName =
         mixinType == null
             ? generatedCodecBinaryName(type)
@@ -490,8 +662,7 @@ public final class JsonSharedRegistry {
   public static GeneratedJsonCodec<?> validateGeneratedCodec(
       Class<?> type, GeneratedJsonCodec<?> codec) {
     if (codec == null) {
-      throw new ForyJsonException(
-          "Generated JSON codec factory returned null for " + type.getName());
+      throw new ForyJsonException("Generated JSON codec is null for " + type.getName());
     }
     Class<?> declaredType;
     JsonFieldAccessor[] accessors;
@@ -526,9 +697,7 @@ public final class JsonSharedRegistry {
     Executable creator =
         validateGeneratedCreator(
             type, memberAccessors, creatorNames, creatorTypes, creatorFactory, record);
-    if (!AndroidSupport.IS_ANDROID
-        && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE
-        && RecordUtils.isRecord(type) != record) {
+    if (!AndroidSupport.IS_ANDROID && RecordUtils.isRecord(type) != record) {
       throw invalidGeneratedCodec(type, "isRecord() does not match the runtime model class");
     }
     codec.initializeValidated(
@@ -928,10 +1097,6 @@ public final class JsonSharedRegistry {
       this.overlay = overlay;
     }
 
-    public boolean isEmpty() {
-      return overlay.isEmpty();
-    }
-
     public Set<AnnotatedElement> sourceDeclarations() {
       return overlay.sourceDeclarations();
     }
@@ -991,7 +1156,8 @@ public final class JsonSharedRegistry {
         }
         Class<?> mixinType = mixinType(targetType);
         boolean loadGeneratedCodec =
-            targetType.getDeclaredAnnotation(JsonType.class) != null || mixinType != null;
+            !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE
+                && (targetType.getDeclaredAnnotation(JsonType.class) != null || mixinType != null);
         GeneratedJsonCodec<?> generatedCodec =
             loadGeneratedCodec ? generatedCodecIfPresent(targetType, mixinType) : null;
         JsonValueDeclaration resolved =
@@ -1161,8 +1327,16 @@ public final class JsonSharedRegistry {
     return newCodec(codecClass, "JSON map key codec");
   }
 
+  @SuppressWarnings("unchecked")
   private static <T> T newCodec(Class<? extends T> codecClass, String role) {
     validateCodecClass(codecClass, role);
+    if (GraalvmSupport.isGraalRuntime()) {
+      try {
+        return (T) ReflectionUtils.getCtrHandle(codecClass, new Class<?>[0]).invoke();
+      } catch (Throwable e) {
+        throw invalidCodecClass(codecClass, role, "constructor failed", e);
+      }
+    }
     Constructor<? extends T> constructor;
     try {
       constructor = codecClass.getConstructor();
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java
index 1cb5cab47..f7b4425a7 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java
@@ -20,7 +20,6 @@
 package org.apache.fory.json.resolver;
 
 import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodType;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Executable;
 import java.lang.reflect.InvocationTargetException;
@@ -28,14 +27,15 @@ import java.lang.reflect.Method;
 import org.apache.fory.json.ForyJsonException;
 import org.apache.fory.json.codec.GeneratedJsonCodec;
 import org.apache.fory.json.codec.JsonValueCodec;
+import org.apache.fory.json.meta.JsonCreatorInfo;
 import org.apache.fory.json.meta.JsonFieldAccessor;
+import org.apache.fory.json.reader.JsonReader;
 import org.apache.fory.json.reader.Latin1JsonReader;
 import org.apache.fory.json.reader.Utf16JsonReader;
 import org.apache.fory.json.reader.Utf8JsonReader;
 import org.apache.fory.json.writer.StringJsonWriter;
 import org.apache.fory.json.writer.Utf8JsonWriter;
 import org.apache.fory.platform.AndroidSupport;
-import org.apache.fory.platform.internal._JDKAccess;
 
 /** Complete String representation selected by one effective {@code JsonValue} member. */
 final class JsonStringValueCodec implements JsonValueCodec<Object> {
@@ -95,7 +95,7 @@ final class JsonStringValueCodec implements JsonValueCodec<Object> {
       reader.readNull();
       return null;
     }
-    return read(reader.readString());
+    return read(reader, reader.readString());
   }
 
   @Override
@@ -104,7 +104,7 @@ final class JsonStringValueCodec implements JsonValueCodec<Object> {
       reader.readNull();
       return null;
     }
-    return read(reader.readString());
+    return read(reader, reader.readString());
   }
 
   @Override
@@ -113,10 +113,10 @@ final class JsonStringValueCodec implements JsonValueCodec<Object> {
       reader.readNull();
       return null;
     }
-    return read(reader.readString());
+    return read(reader, reader.readString());
   }
 
-  private Object read(String value) {
+  private Object read(JsonReader reader, String value) {
     if (raw) {
       throw new ForyJsonException(
           "Combined @JsonValue and @JsonRawValue representation is write-only for "
@@ -128,7 +128,7 @@ final class JsonStringValueCodec implements JsonValueCodec<Object> {
               + ownerType.getName()
               + " requires a one-String-argument @JsonCreator");
     }
-    return creator.create(value);
+    return creator.create(reader, value);
   }
 
   private abstract static class ValueCreator {
@@ -138,7 +138,7 @@ final class JsonStringValueCodec implements JsonValueCodec<Object> {
       this.ownerType = ownerType;
     }
 
-    abstract Object create(String value);
+    abstract Object create(JsonReader reader, String value);
 
     final Object requireResult(Object result) {
       if (result == null || result.getClass() != ownerType) {
@@ -148,11 +148,6 @@ final class JsonStringValueCodec implements JsonValueCodec<Object> {
       return result;
     }
 
-    final ForyJsonException invocationFailure(Throwable cause) {
-      return new ForyJsonException(
-          "Failed to invoke JSON creator for " + ownerType.getName(), cause);
-    }
-
     final ForyJsonException creatorFailure(Throwable cause) {
       if (cause instanceof Error) {
         throw (Error) cause;
@@ -166,7 +161,7 @@ final class JsonStringValueCodec implements JsonValueCodec<Object> {
         return new GeneratedCreator(ownerType, generatedCodec);
       }
       if (!AndroidSupport.IS_ANDROID) {
-        return new MethodHandleCreator(ownerType, buildInvoker(ownerType, executable));
+        return new MethodHandleCreator(ownerType, buildInvoker(executable));
       }
       executable.setAccessible(true);
       return executable instanceof Constructor
@@ -174,17 +169,8 @@ final class JsonStringValueCodec implements JsonValueCodec<Object> {
           : new FactoryCreator(ownerType, (Method) executable);
     }
 
-    private static MethodHandle buildInvoker(Class<?> ownerType, Executable executable) {
-      try {
-        MethodHandle target =
-            executable instanceof Constructor
-                ? _JDKAccess._trustedLookup(ownerType)
-                    .unreflectConstructor((Constructor<?>) executable)
-                : _JDKAccess._trustedLookup(ownerType).unreflect((Method) executable);
-        return target.asType(MethodType.methodType(Object.class, String.class));
-      } catch (IllegalAccessException e) {
-        throw new ForyJsonException("Cannot access JSON creator for " + ownerType.getName(), e);
-      }
+    private static MethodHandle buildInvoker(Executable executable) {
+      return JsonCreatorInfo.stringCreatorHandle(executable);
     }
   }
 
@@ -197,11 +183,13 @@ final class JsonStringValueCodec implements JsonValueCodec<Object> {
     }
 
     @Override
-    Object create(String value) {
+    Object create(JsonReader reader, String value) {
       try {
-        return requireResult(generatedCodec.newInstance(new Object[] {value}));
+        return requireResult(generatedCodec.newInstance(reader.creatorArguments(value)));
       } catch (Throwable cause) {
         throw creatorFailure(cause);
+      } finally {
+        reader.clearCreatorArguments();
       }
     }
   }
@@ -215,7 +203,7 @@ final class JsonStringValueCodec implements JsonValueCodec<Object> {
     }
 
     @Override
-    Object create(String value) {
+    Object create(JsonReader reader, String value) {
       try {
         Object result = (Object) invoker.invokeExact(value);
         return requireResult(result);
@@ -234,13 +222,15 @@ final class JsonStringValueCodec implements JsonValueCodec<Object> {
     }
 
     @Override
-    Object create(String value) {
+    Object create(JsonReader reader, String value) {
       try {
-        return requireResult(constructor.newInstance(value));
+        return requireResult(constructor.newInstance(reader.creatorArguments(value)));
       } catch (InstantiationException | IllegalAccessException e) {
-        throw invocationFailure(e);
+        throw creatorFailure(e);
       } catch (InvocationTargetException e) {
         throw creatorFailure(e.getCause());
+      } finally {
+        reader.clearCreatorArguments();
       }
     }
   }
@@ -254,13 +244,15 @@ final class JsonStringValueCodec implements JsonValueCodec<Object> {
     }
 
     @Override
-    Object create(String value) {
+    Object create(JsonReader reader, String value) {
       try {
-        return requireResult(factory.invoke(null, value));
+        return requireResult(factory.invoke(null, reader.creatorArguments(value)));
       } catch (IllegalAccessException e) {
-        throw invocationFailure(e);
+        throw creatorFailure(e);
       } catch (InvocationTargetException e) {
         throw creatorFailure(e.getCause());
+      } finally {
+        reader.clearCreatorArguments();
       }
     }
   }
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
index c908b6f72..127557376 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
@@ -67,6 +67,8 @@ import org.apache.fory.json.meta.JsonCreatorInfo;
 import org.apache.fory.json.meta.JsonFieldInfo;
 import org.apache.fory.json.meta.JsonFieldKind;
 import org.apache.fory.json.meta.JsonFieldTable;
+import org.apache.fory.logging.Logger;
+import org.apache.fory.logging.LoggerFactory;
 import org.apache.fory.reflect.TypeRef;
 
 /**
@@ -88,6 +90,12 @@ import org.apache.fory.reflect.TypeRef;
  * raw-class JIT dispatch.
  */
 public final class JsonTypeResolver {
+  private static final Logger LOG = LoggerFactory.getLogger(JsonTypeResolver.class);
+  private static final String NATIVE_INTERPRETER_MESSAGE =
+      "Fory JSON is using interpreted codecs because the current configuration was not included "
+          + "in this native image. Return this configuration from a reachable "
+          + "@ForyJsonProvider to enable generated codecs.";
+
   private final Map<Object, ObjectCodec<?>> objectCodecs;
   private final Map<Object, JsonTypeInfo> typeInfos;
   private final JsonSharedRegistry sharedRegistry;
@@ -348,6 +356,21 @@ public final class JsonTypeResolver {
     }
   }
 
+  /** Generates every hosted capability rooted at an explicitly selected Native Image model. */
+  @Internal
+  public void generateHostedCodecs(Class<?> type) {
+    if (!sharedRegistry.hostedCodegen()) {
+      throw new IllegalStateException("Hosted JSON codec generation requires a hosted registry");
+    }
+    JsonTypeInfo typeInfo = getTypeInfo(type, type);
+    ArrayList<JsonTypeInfo> roots = new ArrayList<>(1);
+    roots.add(typeInfo);
+    // A preceding selected model may already have resolved this type inside an uncodegenable graph.
+    // Cached metadata is still a generation root; otherwise that earlier graph can suppress every
+    // capability for an independently eligible annotated model.
+    requestCapabilities(roots);
+  }
+
   private JsonTypeInfo resolveTypeInfo(Type declaredType, Class<?> rawType, Object key) {
     JsonTypeInfo typeInfo = customTypeInfo(declaredType, rawType);
     if (typeInfo != null) {
@@ -646,7 +669,7 @@ public final class JsonTypeResolver {
   }
 
   private void completeResolution(ResolutionSnapshot snapshot) {
-    if (snapshot == null || codegen == null) {
+    if (snapshot == null) {
       return;
     }
     ArrayList<JsonTypeInfo> roots = new ArrayList<>();
@@ -655,9 +678,25 @@ public final class JsonTypeResolver {
         roots.add(entry.getValue());
       }
     }
-    if (!roots.isEmpty()) {
-      requestCapabilities(roots);
+    if (roots.isEmpty()) {
+      return;
+    }
+    if (!sharedRegistry.generatedCapabilitiesEnabled()) {
+      if (sharedRegistry.missingNativeConfiguration() && containsObjectModel(roots)) {
+        LOG.warnOnce(NATIVE_INTERPRETER_MESSAGE);
+      }
+      return;
+    }
+    requestCapabilities(roots);
+  }
+
+  private boolean containsObjectModel(ArrayList<JsonTypeInfo> roots) {
+    for (int i = 0; i < roots.size(); i++) {
+      if (canonicalObjectOwner(roots.get(i)) != null) {
+        return true;
+      }
     }
+    return false;
   }
 
   private void rollbackResolution(ResolutionSnapshot snapshot) {
@@ -1468,9 +1507,42 @@ public final class JsonTypeResolver {
   }
 
   private boolean canCompile(ObjectCodec<?> owner, CapabilityKind kind) {
-    return kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER
-        ? codegen.canCompileWriter(owner)
-        : codegen.canCompileReader(owner);
+    if (nativeObjectClass(owner.type(), kind) != null) {
+      return true;
+    }
+    return codegen != null
+        && (kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER
+            ? codegen.canCompileWriter(owner)
+            : codegen.canCompileReader(owner));
+  }
+
+  private boolean canCompileCollection(JsonTypeInfo typeInfo, CapabilityKind kind) {
+    Type type = typeInfo.type();
+    boolean generated =
+        kind == CapabilityKind.UTF8_WRITER
+            ? sharedRegistry.nativeUtf8CollectionWriterClass(type) != null
+            : sharedRegistry.nativeUtf8CollectionReaderClass(type) != null;
+    if (generated) {
+      return true;
+    }
+    return codegen != null;
+  }
+
+  private Class<?> nativeObjectClass(Class<?> type, CapabilityKind kind) {
+    switch (kind) {
+      case STRING_WRITER:
+        return sharedRegistry.nativeStringWriterClass(type);
+      case UTF8_WRITER:
+        return sharedRegistry.nativeUtf8WriterClass(type);
+      case LATIN1_READER:
+        return sharedRegistry.nativeLatin1ReaderClass(type);
+      case UTF16_READER:
+        return sharedRegistry.nativeUtf16ReaderClass(type);
+      case UTF8_READER:
+        return sharedRegistry.nativeUtf8ReaderClass(type);
+      default:
+        throw new IllegalStateException("Unknown JSON capability kind " + kind);
+    }
   }
 
   private static Object currentCapability(JsonTypeInfo typeInfo, CapabilityKind kind) {
@@ -1519,6 +1591,22 @@ public final class JsonTypeResolver {
     }
   }
 
+  private Class<?> nativeGeneratedClass(CapabilityNode node, CapabilityKind kind) {
+    if (node.subtypeOwner != null) {
+      throw new IllegalStateException("Inline subtype readers reuse child generated classes");
+    }
+    if (node.collectionOwner != null) {
+      if (kind == CapabilityKind.UTF8_WRITER) {
+        return sharedRegistry.nativeUtf8CollectionWriterClass(node.typeInfo.type());
+      }
+      if (kind == CapabilityKind.UTF8_READER) {
+        return sharedRegistry.nativeUtf8CollectionReaderClass(node.typeInfo.type());
+      }
+      throw new IllegalStateException("Unsupported generated JSON collection capability " + kind);
+    }
+    return nativeObjectClass(node.objectOwner.type(), kind);
+  }
+
   private Object newCapability(
       CapabilityNode node,
       Class<?> generatedClass,
@@ -1712,13 +1800,33 @@ public final class JsonTypeResolver {
   private void requestCapabilities(ArrayList<JsonTypeInfo> roots) {
     for (CapabilityKind kind : CapabilityKind.values()) {
       CapabilityGraph graph = new CapabilityGraph(kind);
-      if (graph.addRoots(roots) && !graph.ordered.isEmpty()) {
+      for (int i = 0; i < roots.size(); i++) {
+        JsonTypeInfo root = roots.get(i);
+        // Probe each cold root independently so an interpreter-only graph cannot reject unrelated
+        // eligible roots. Successful roots are then rebuilt into one graph to preserve the existing
+        // atomic parent/child publication boundary.
+        CapabilityGraph candidate = new CapabilityGraph(kind);
+        if (candidate.addDependency(root) && !graph.addDependency(root)) {
+          throw new IllegalStateException(
+              "Cannot merge eligible JSON capability graph for " + root.type());
+        }
+      }
+      if (!graph.ordered.isEmpty()) {
         requestGraph(graph);
       }
     }
   }
 
   private void requestGraph(CapabilityGraph graph) {
+    if (sharedRegistry.hostedCodegen()) {
+      graph.classesReady().join();
+      return;
+    }
+    if (sharedRegistry.nativeGeneratedClasses()) {
+      graph.loadNativeClasses();
+      graph.publish();
+      return;
+    }
     jitContext.registerJITFuture(
         () -> graph.classesReady().thenApply(ignored -> graph),
         new JsonJITContext.JITCallback<CapabilityGraph>() {
@@ -1752,15 +1860,6 @@ public final class JsonTypeResolver {
       this.kind = kind;
     }
 
-    private boolean addRoots(ArrayList<JsonTypeInfo> roots) {
-      for (int i = 0; i < roots.size(); i++) {
-        if (!addDependency(roots.get(i))) {
-          return false;
-        }
-      }
-      return true;
-    }
-
     private boolean addDependency(JsonTypeInfo typeInfo) {
       return addDependency(typeInfo, false);
     }
@@ -1845,7 +1944,7 @@ public final class JsonTypeResolver {
         return existing.complete;
       }
       JsonTypeInfo element = declaredCollectionElement(typeInfo);
-      if (element == null) {
+      if (element == null || !canCompileCollection(typeInfo, kind)) {
         return false;
       }
       CapabilityNode node = new CapabilityNode(typeInfo, owner, initial);
@@ -1865,12 +1964,34 @@ public final class JsonTypeResolver {
         if (node.subtypeOwner != null) {
           continue;
         }
+        if (sharedRegistry.hostedCodegen()) {
+          // Another provider loader may already have generated this capability under the same
+          // source key. Reuse it while still walking the graph so this loader can add missing
+          // types.
+          node.generatedClass = nativeGeneratedClass(node, kind);
+          if (node.generatedClass != null) {
+            continue;
+          }
+        }
         node.classFuture = generatedClass(node, kind);
         futures.add(node.classFuture);
       }
       return CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]));
     }
 
+    private void loadNativeClasses() {
+      for (int i = 0; i < ordered.size(); i++) {
+        CapabilityNode node = ordered.get(i);
+        if (node.subtypeOwner == null) {
+          node.generatedClass = nativeGeneratedClass(node, kind);
+          if (node.generatedClass == null) {
+            throw new IllegalStateException(
+                "Missing generated Fory JSON class for " + node.typeInfo.type());
+          }
+        }
+      }
+    }
+
     private void publish() {
       requireJITLock();
       IdentityMap<JsonTypeInfo, Object> capabilities = new IdentityMap<>();
@@ -1889,7 +2010,8 @@ public final class JsonTypeResolver {
         }
         Class<?> generatedClass = null;
         if (node.subtypeOwner == null) {
-          generatedClass = node.classFuture.getNow(null);
+          generatedClass =
+              node.generatedClass == null ? node.classFuture.getNow(null) : node.generatedClass;
           if (generatedClass == null) {
             throw new IllegalStateException("Generated JSON class is not ready");
           }
@@ -1921,6 +2043,7 @@ public final class JsonTypeResolver {
     private final Object initial;
     private boolean complete;
     private CompletableFuture<Class<?>> classFuture;
+    private Class<?> generatedClass;
     private Object instance;
 
     private CapabilityNode(JsonTypeInfo typeInfo, ObjectCodec<Object> owner, Object initial) {
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java
index b4e3fb698..6193ac5b8 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java
@@ -174,10 +174,7 @@ final class JsonValueDeclaration {
         && (registry.annotation(type, field, JsonRawValue.class) != null)
             == (registry.annotation(type, method, JsonRawValue.class) != null)) {
       members.clear();
-      members.add(
-          generatedCodec != null && generatedCodec.validatedAccessor(method) != null
-              ? method
-              : field);
+      members.add(method);
     }
   }
 
diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/codec/ForyJsonGraalVMFeature.java b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java
similarity index 59%
rename from java/fory-json/src/main/java17/org/apache/fory/json/codec/ForyJsonGraalVMFeature.java
rename to java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java
index 1f99b4cba..507833117 100644
--- a/java/fory-json/src/main/java17/org/apache/fory/json/codec/ForyJsonGraalVMFeature.java
+++ b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java
@@ -17,8 +17,9 @@
  * under the License.
  */
 
-package org.apache.fory.json.codec;
+package org.apache.fory.json;
 
+import java.lang.invoke.MethodHandle;
 import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Executable;
@@ -28,18 +29,27 @@ import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.lang.reflect.Parameter;
 import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.RecordComponent;
 import java.lang.reflect.Type;
 import java.lang.reflect.TypeVariable;
 import java.lang.reflect.WildcardType;
 import java.math.BigDecimal;
 import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
 import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.annotation.ForyJsonProvider;
+import org.apache.fory.json.annotation.JsonAnySetter;
 import org.apache.fory.json.annotation.JsonBase64;
 import org.apache.fory.json.annotation.JsonCodec;
 import org.apache.fory.json.annotation.JsonCreator;
@@ -48,17 +58,25 @@ import org.apache.fory.json.annotation.JsonSubTypes;
 import org.apache.fory.json.annotation.JsonType;
 import org.apache.fory.json.annotation.JsonUnwrapped;
 import org.apache.fory.json.annotation.JsonValue;
-import org.apache.fory.json.resolver.GeneratedJsonCodecFactories;
+import org.apache.fory.json.codec.Base64ByteArrayCodec;
+import org.apache.fory.json.codec.ObjectCodec;
+import org.apache.fory.json.codegen.JsonCodegenKey;
+import org.apache.fory.json.meta.JsonCreatorInfo;
+import org.apache.fory.json.meta.JsonFieldAccessor;
+import org.apache.fory.json.resolver.JsonGeneratedClassRegistry;
 import org.apache.fory.json.resolver.JsonSharedRegistry;
 import org.apache.fory.json.resolver.JsonSharedRegistry.JsonMixinView;
+import org.apache.fory.json.resolver.JsonTypeResolver;
 import org.apache.fory.platform.GraalvmSupport;
+import org.apache.fory.platform.internal._JDKAccess;
+import org.apache.fory.reflect.ObjectInstantiators;
 import org.apache.fory.reflect.ReflectionUtils;
 import org.apache.fory.reflect.TypeRef;
+import org.apache.fory.util.record.RecordUtils;
 import org.graalvm.nativeimage.hosted.Feature;
-import org.graalvm.nativeimage.hosted.RuntimeClassInitialization;
 import org.graalvm.nativeimage.hosted.RuntimeReflection;
 
-/** Registers reachable Fory JSON models for GraalVM native image reflection. */
+/** Prepares reachable Fory JSON models and provider-selected codecs for Native Image. */
 final class ForyJsonGraalVMFeature implements Feature {
   private static final String[] SQL_TYPES = {
     "java.sql.Date", "java.sql.Time", "java.sql.Timestamp"
@@ -69,27 +87,24 @@ final class ForyJsonGraalVMFeature implements Feature {
   private final Set<Class<?>> processedDeclarations = ConcurrentHashMap.newKeySet();
   private final Set<Class<?>> processedModels = ConcurrentHashMap.newKeySet();
   private final Set<Class<?>> processedMixins = ConcurrentHashMap.newKeySet();
+  private final Map<Class<?>, Set<Class<?>>> reachableMixins = new LinkedHashMap<>();
+  private final Set<Class<?>> processedProviders = ConcurrentHashMap.newKeySet();
   private final Set<Class<?>> processedCodecs = ConcurrentHashMap.newKeySet();
   private final Set<Class<?>> processedContainers = ConcurrentHashMap.newKeySet();
+  private final Set<Executable> processedCreators = new LinkedHashSet<>();
+  // JsonCodegenKey stays loader-free so runtime configurations can reproduce it. Hosted resolvers
+  // remain loader-specific, while JsonGeneratedClassRegistry merges their generated capabilities.
+  private final Map<JsonCodegenKey, ArrayList<HostedConfiguration>> hostedConfigurations =
+      new LinkedHashMap<>();
 
   @Override
   public String getDescription() {
-    return "Registers reachable Fory JSON models for GraalVM native image";
+    return "Prepares reachable Fory JSON models for GraalVM Native Image";
   }
 
   @Override
   public void beforeAnalysis(BeforeAnalysisAccess access) {
-    RuntimeClassInitialization.initializeAtBuildTime(GeneratedJsonCodecFactories.class);
-    // Exact target-Mixin keys are retained in the frozen factory table and therefore become image
-    // heap objects together with the table. Derive the private key name from its enclosing class so
-    // relocated/shaded packages remain valid.
-    RuntimeClassInitialization.initializeAtBuildTime(
-        GeneratedJsonCodecFactories.class.getName() + "$Key");
-    // Hosted Mixin discovery deliberately reuses the runtime's structural resolver so build-time
-    // reachability and runtime semantics cannot drift. Its static state contains only the immutable
-    // supported-annotation set and is therefore safe to initialize in the image builder.
-    RuntimeClassInitialization.initializeAtBuildTime(
-        ForyJson.class.getPackage().getName() + ".resolver.JsonMixinAnnotations");
+    // native-image.properties owns class initialization; this Feature owns reachability metadata.
     access.registerSubtypeReachabilityHandler(this::processReachableType, Object.class);
   }
 
@@ -103,7 +118,9 @@ final class ForyJsonGraalVMFeature implements Feature {
       return;
     }
     boolean changed = false;
-    for (Class<?> type : reachableTypes) {
+    List<Class<?>> orderedTypes = new ArrayList<>(reachableTypes);
+    orderedTypes.sort(Comparator.comparing(Class::getName));
+    for (Class<?> type : orderedTypes) {
       if (processedReachableTypes.add(type)) {
         changed |= registerContainer(type);
         changed |= registerDeclarations(type);
@@ -114,17 +131,189 @@ final class ForyJsonGraalVMFeature implements Feature {
         if (mixin != null) {
           changed |= registerMixin(access, type, mixin.target());
         }
+        if (type.getDeclaredAnnotation(ForyJsonProvider.class) != null) {
+          changed |= registerProvider(type);
+        }
         if (type == ForyJson.class) {
           registerBuiltInTypes(access);
           changed = true;
         }
       }
     }
+    changed |= generateConfigurations(access);
     if (changed) {
       access.requireAnalysisIteration();
     }
   }
 
+  private boolean registerProvider(Class<?> providerClass) {
+    if (!processedProviders.add(providerClass)) {
+      return false;
+    }
+    int modifiers = providerClass.getModifiers();
+    if (!Modifier.isPublic(modifiers)
+        || Modifier.isAbstract(modifiers)
+        || providerClass.isInterface()
+        || providerClass.isEnum()) {
+      throw providerFailure(providerClass, "must be a public concrete class", null);
+    }
+    Constructor<?> constructor;
+    try {
+      constructor = providerClass.getConstructor();
+    } catch (NoSuchMethodException e) {
+      throw providerFailure(providerClass, "must have a public no-argument constructor", e);
+    }
+    MethodHandle providerConstructor = providerConstructor(providerClass, constructor);
+    Object provider;
+    try {
+      provider = providerConstructor.invoke();
+    } catch (Throwable e) {
+      throw providerFailure(providerClass, "cannot be constructed", e);
+    }
+    List<Method> methods = providerMethods(providerClass);
+    if (methods.isEmpty()) {
+      throw providerFailure(providerClass, "does not declare an effective ForyJson method", null);
+    }
+    boolean changed = false;
+    for (Method method : methods) {
+      MethodHandle providerMethod = providerMethod(providerClass, method);
+      ForyJson json;
+      try {
+        json = (ForyJson) providerMethod.invoke(provider);
+      } catch (Throwable e) {
+        throw providerFailure(providerClass, "cannot invoke provider method " + method, e);
+      }
+      if (json == null) {
+        throw providerFailure(providerClass, "provider method returned null: " + method, null);
+      }
+      JsonConfig config = json.config();
+      if (!config.codegenEnabled()) {
+        throw providerFailure(
+            providerClass, "provider method returned a codegen-disabled ForyJson: " + method, null);
+      }
+      JsonCodegenKey key = config.codegenKey();
+      ArrayList<HostedConfiguration> configurations = hostedConfigurations.get(key);
+      if (configurations == null) {
+        configurations = new ArrayList<>();
+        hostedConfigurations.put(key, configurations);
+      }
+      ClassLoader classLoader = config.classLoader();
+      boolean knownLoader = false;
+      for (HostedConfiguration configuration : configurations) {
+        if (configuration.classLoader == classLoader) {
+          knownLoader = true;
+          break;
+        }
+      }
+      if (!knownLoader) {
+        configurations.add(new HostedConfiguration(config));
+        changed = true;
+      }
+    }
+    return changed;
+  }
+
+  private static MethodHandle providerConstructor(
+      Class<?> providerClass, Constructor<?> constructor) {
+    try {
+      return _JDKAccess._trustedLookup(providerClass).unreflectConstructor(constructor);
+    } catch (IllegalAccessException e) {
+      throw providerFailure(providerClass, "cannot access its constructor", e);
+    }
+  }
+
+  private static MethodHandle providerMethod(Class<?> providerClass, Method method) {
+    try {
+      return _JDKAccess._trustedLookup(method.getDeclaringClass()).unreflect(method);
+    } catch (IllegalAccessException e) {
+      throw providerFailure(providerClass, "cannot access method " + method, e);
+    }
+  }
+
+  private static List<Method> providerMethods(Class<?> providerClass) {
+    Map<MethodSignature, Method> effective = new HashMap<>();
+    for (Method method : providerClass.getMethods()) {
+      if (method.isBridge()
+          || method.isSynthetic()
+          || Modifier.isStatic(method.getModifiers())
+          || method.getParameterCount() != 0
+          || method.getReturnType() != ForyJson.class) {
+        continue;
+      }
+      MethodSignature signature = new MethodSignature(method);
+      Method previous = effective.get(signature);
+      if (previous == null
+          || previous.getDeclaringClass().isAssignableFrom(method.getDeclaringClass())) {
+        effective.put(signature, method);
+      } else if (!method.getDeclaringClass().isAssignableFrom(previous.getDeclaringClass())
+          && method.getDeclaringClass().getName().compareTo(previous.getDeclaringClass().getName())
+              < 0) {
+        effective.put(signature, method);
+      }
+    }
+    ArrayList<Method> methods = new ArrayList<>(effective.values());
+    methods.sort(
+        Comparator.comparing(Method::getName)
+            .thenComparing(method -> method.getDeclaringClass().getName())
+            .thenComparing(Method::toGenericString));
+    return methods;
+  }
+
+  private boolean generateConfigurations(DuringAnalysisAccess access) {
+    boolean changed = false;
+    for (Map.Entry<JsonCodegenKey, ArrayList<HostedConfiguration>> entry :
+        hostedConfigurations.entrySet()) {
+      for (HostedConfiguration configuration : entry.getValue()) {
+        LinkedHashSet<Class<?>> selectedModels = new LinkedHashSet<>(processedModels);
+        for (Map.Entry<Class<?>, Set<Class<?>>> mixin : reachableMixins.entrySet()) {
+          if (mixin.getValue().contains(configuration.mixins.get(mixin.getKey()))) {
+            selectedModels.add(mixin.getKey());
+          }
+        }
+        ArrayList<Class<?>> models = new ArrayList<>(selectedModels);
+        models.sort(Comparator.comparing(Class::getName));
+        for (Class<?> model : models) {
+          if (!configuration.processedModels.add(model)) {
+            continue;
+          }
+          try {
+            configuration.resolver.generateHostedCodecs(model);
+          } catch (RuntimeException | LinkageError e) {
+            throw new IllegalStateException(
+                "Cannot generate Fory JSON codecs for " + model.getName(), e);
+          }
+          Set<Class<?>> generatedClasses =
+              JsonGeneratedClassRegistry.register(entry.getKey(), configuration.registry);
+          for (Class<?> generatedClass : generatedClasses) {
+            registerGeneratedClass(generatedClass);
+          }
+          changed = true;
+        }
+      }
+    }
+    return changed;
+  }
+
+  private void registerGeneratedClass(Class<?> generatedClass) {
+    Constructor<?>[] constructors = generatedClass.getDeclaredConstructors();
+    if (constructors.length == 0) {
+      throw new IllegalStateException(
+          "Generated Fory JSON class has no constructor: " + generatedClass.getName());
+    }
+    for (Constructor<?> constructor : constructors) {
+      ReflectionUtils.getCtrHandle(
+          constructor.getDeclaringClass(), constructor.getParameterTypes());
+    }
+  }
+
+  private static IllegalStateException providerFailure(
+      Class<?> providerClass, String reason, Throwable cause) {
+    String message = "Invalid @ForyJsonProvider " + providerClass.getName() + ": " + reason;
+    return cause == null
+        ? new IllegalStateException(message)
+        : new IllegalStateException(message, cause);
+  }
+
   private boolean registerModel(DuringAnalysisAccess access, Class<?> type) {
     if (!processedModels.add(type)) {
       return false;
@@ -132,16 +321,30 @@ final class ForyJsonGraalVMFeature implements Feature {
     RuntimeReflection.register(type);
     registerContainer(type);
     registerDeclarations(type);
-    if (!type.isEnum()
-        && !Collection.class.isAssignableFrom(type)
-        && !Map.class.isAssignableFrom(type)) {
+    JsonCodec directTypeCodec = type.getDeclaredAnnotation(JsonCodec.class);
+    boolean hasTypeCodec = directTypeCodec != null || hasInheritedTypeCodec(type, null);
+    boolean hasJsonValue =
+        (!hasTypeCodec || isCompleteTypeCodec(directTypeCodec))
+            && registerJsonValueDeclarations(access, type, null);
+    JsonSubTypes subTypes = type.getDeclaredAnnotation(JsonSubTypes.class);
+    boolean intrinsicType =
+        type.isEnum()
+            || Collection.class.isAssignableFrom(type)
+            || Map.class.isAssignableFrom(type);
+    if (!intrinsicType && !hasTypeCodec && !hasJsonValue && subTypes == null) {
       registerModelHierarchy(access, type);
-      if (!type.isRecord() && GraalvmSupport.needReflectionRegisterForCreation(type)) {
-        RuntimeReflection.registerForReflectiveInstantiation(type);
+      if (type.isRecord()) {
+        registerRecord(type);
+      } else if (!Modifier.isAbstract(type.getModifiers())) {
+        ObjectInstantiators.getObjectInstantiator(type);
+        if (GraalvmSupport.needReflectionRegisterForCreation(type)) {
+          RuntimeReflection.registerForReflectiveInstantiation(type);
+        }
       }
     }
-    registerGeneratedCodec(access, type, null);
-    registerSubtypes(access, type);
+    if (!hasTypeCodec && !hasJsonValue) {
+      registerSubtypes(access, type);
+    }
     return true;
   }
 
@@ -150,11 +353,9 @@ final class ForyJsonGraalVMFeature implements Feature {
     if (!processedMixins.add(mixinType)) {
       return false;
     }
+    reachableMixins.computeIfAbsent(targetType, ignored -> new LinkedHashSet<>()).add(mixinType);
     JsonMixinView annotations = JsonSharedRegistry.resolveMixin(targetType, mixinType);
     RuntimeReflection.register(mixinType);
-    if (annotations.isEmpty()) {
-      return true;
-    }
     registerReflectiveDeclarations(annotations.sourceDeclarations());
     registerReflectiveDeclarations(annotations.targetDeclarations());
     // Retain every directly declared hierarchy codec plus the exact Mixin replacement. Runtime
@@ -179,12 +380,15 @@ final class ForyJsonGraalVMFeature implements Feature {
     // effective annotations visible here as hosted reachability facts.
     if (!intrinsicTarget && !hasTypeCodec && !hasJsonValue && subTypes == null) {
       registerModelHierarchy(access, targetType, annotations);
-      if (!targetType.isRecord()
-          && GraalvmSupport.needReflectionRegisterForCreation(targetType)) {
-        RuntimeReflection.registerForReflectiveInstantiation(targetType);
+      if (targetType.isRecord()) {
+        registerRecord(targetType);
+      } else if (!Modifier.isAbstract(targetType.getModifiers())) {
+        ObjectInstantiators.getObjectInstantiator(targetType);
+        if (GraalvmSupport.needReflectionRegisterForCreation(targetType)) {
+          RuntimeReflection.registerForReflectiveInstantiation(targetType);
+        }
       }
     }
-    registerGeneratedCodec(access, targetType, mixinType);
     if (!hasTypeCodec && !hasJsonValue) {
       registerSubtypes(access, targetType, annotations);
     }
@@ -226,7 +430,10 @@ final class ForyJsonGraalVMFeature implements Feature {
         if (annotation(annotations, field, JsonValue.class) != null) {
           hasValue = true;
           RuntimeReflection.register(field);
-          if (Runtime.version().feature() <= 24) {
+          if (!field.getDeclaringClass().isRecord()) {
+            JsonFieldAccessor.forField(field);
+          }
+          if (!field.getDeclaringClass().isRecord() && Runtime.version().feature() <= 24) {
             access.registerAsUnsafeAccessed(field);
           }
           registerOccurrenceCodecs(annotations, field);
@@ -237,6 +444,7 @@ final class ForyJsonGraalVMFeature implements Feature {
       if (annotation(annotations, method, JsonValue.class) != null) {
         hasValue = true;
         RuntimeReflection.register(method);
+        JsonFieldAccessor.forGetter(method);
         registerOccurrenceCodecs(annotations, method);
       }
     }
@@ -248,6 +456,7 @@ final class ForyJsonGraalVMFeature implements Feature {
             && annotation(annotations, method, JsonValue.class) != null) {
           hasValue = true;
           RuntimeReflection.register(method);
+          JsonFieldAccessor.forGetter(method);
           registerOccurrenceCodecs(annotations, method);
         }
       }
@@ -255,14 +464,17 @@ final class ForyJsonGraalVMFeature implements Feature {
     if (!hasValue) {
       return false;
     }
+    if (type.isRecord()) {
+      registerRecord(type);
+    }
     for (Constructor<?> constructor : type.getDeclaredConstructors()) {
       if (annotation(annotations, constructor, JsonCreator.class) != null) {
-        RuntimeReflection.register(constructor);
+        registerCreator(constructor);
       }
     }
     for (Method method : type.getDeclaredMethods()) {
       if (annotation(annotations, method, JsonCreator.class) != null) {
-        RuntimeReflection.register(method);
+        registerCreator(method);
       }
     }
     return true;
@@ -282,61 +494,9 @@ final class ForyJsonGraalVMFeature implements Feature {
     }
   }
 
-  private void registerGeneratedCodec(
-      DuringAnalysisAccess access, Class<?> type, Class<?> mixinType) {
-    String codecName =
-        mixinType == null
-            ? JsonSharedRegistry.generatedCodecBinaryName(type)
-            : JsonSharedRegistry.generatedMixinCodecBinaryName(mixinType, type);
-    Class<?> codecClass = access.findClassByName(codecName);
-    if (codecClass == null) {
-      return;
-    }
-    if (!GeneratedJsonCodec.class.isAssignableFrom(codecClass)) {
-      throw new IllegalStateException(
-          codecName + " does not extend " + GeneratedJsonCodec.class.getName());
-    }
-    try {
-      if (!Modifier.isPublic(codecClass.getModifiers())) {
-        throw new IllegalStateException("Generated JSON codec must be public: " + codecName);
-      }
-      codecClass.getConstructor();
-    } catch (NoSuchMethodException e) {
-      throw new IllegalStateException(
-          "Generated JSON codec must have a public no-argument constructor: " + codecName, e);
-    }
-    String factoryName = codecName + "$Factory";
-    Class<?> factoryClass = access.findClassByName(factoryName);
-    if (factoryClass == null || !GeneratedJsonCodecFactory.class.isAssignableFrom(factoryClass)) {
-      throw new IllegalStateException(
-          "Missing generated JSON codec factory " + factoryName + " for " + type.getName());
-    }
-    int modifiers = factoryClass.getModifiers();
-    if (!Modifier.isPublic(modifiers)
-        || !Modifier.isStatic(modifiers)
-        || !Modifier.isFinal(modifiers)) {
-      throw new IllegalStateException(
-          "Generated JSON codec factory must be public static final: " + factoryName);
-    }
-    try {
-      GeneratedJsonCodecFactory factory =
-          (GeneratedJsonCodecFactory) ReflectionUtils.getCtrHandle(factoryClass).invoke();
-      GeneratedJsonCodec<?> codec =
-          JsonSharedRegistry.validateGeneratedCodec(type, factory.create());
-      if (codec.validatedRecord() != type.isRecord()) {
-        throw new IllegalStateException(
-            "Generated JSON codec Record metadata does not match " + type.getName());
-      }
-      GeneratedJsonCodecFactories.register(type, mixinType, factory);
-    } catch (Throwable e) {
-      throw new IllegalStateException(
-          "Cannot initialize generated JSON codec factory " + factoryName, e);
-    }
-  }
-
   @Override
   public void afterAnalysis(AfterAnalysisAccess access) {
-    GeneratedJsonCodecFactories.freeze();
+    JsonGeneratedClassRegistry.freeze();
   }
 
   private void registerModelHierarchy(DuringAnalysisAccess access, Class<?> type) {
@@ -350,16 +510,17 @@ final class ForyJsonGraalVMFeature implements Feature {
     for (Class<?> current = type;
         current != null && current != Object.class;
         current = current.getSuperclass()) {
-      // ObjectCodecBuilder still reads semantic annotations, generic types, and exact creator
-      // signatures at image runtime. Register that metadata here instead of routing JSON models
-      // through the core Fory feature, whose Record registration initializes RecordUtils and would
-      // reintroduce the native-Record path that generated JSON companions replace.
+      // Runtime configurations still select members from semantic reflection metadata. Accessor
+      // construction itself is hosted and cached below.
       RuntimeReflection.register(current);
       RuntimeReflection.register(current.getDeclaredFields());
       RuntimeReflection.register(current.getDeclaredMethods());
       RuntimeReflection.register(current.getDeclaredConstructors());
       for (Field field : current.getDeclaredFields()) {
         if (isJsonField(field)) {
+          if (!record) {
+            JsonFieldAccessor.forField(field);
+          }
           if (!current.isRecord() && Runtime.version().feature() <= 24) {
             access.registerAsUnsafeAccessed(field);
           }
@@ -371,15 +532,20 @@ final class ForyJsonGraalVMFeature implements Feature {
           }
         }
       }
+      for (Method method : current.getDeclaredMethods()) {
+        if (annotation(annotations, method, JsonValue.class) != null) {
+          resolveMethodAccessors(annotations, method);
+        }
+      }
     }
     for (Method method : type.getMethods()) {
       boolean mixinSelector = hasMixinSelector(annotations, method);
-      if (ObjectCodecBuilder.usesJsonMetadata(method, record)
-          || mixinSelector) {
+      if (ObjectCodec.usesJsonMetadata(method, record) || mixinSelector) {
+        resolveMethodAccessors(annotations, method);
         if (method.getDeclaringClass().isInterface()) {
           RuntimeReflection.register(method);
         }
-        if (ObjectCodecBuilder.usesJsonReturn(method)
+        if (ObjectCodec.usesJsonReturn(method)
             || mixinSelector && method.getReturnType() != void.class) {
           registerOccurrenceCodecs(annotations, method);
           Type resolvedType = ownerType.resolveType(method.getGenericReturnType()).getType();
@@ -388,7 +554,7 @@ final class ForyJsonGraalVMFeature implements Feature {
             registerNestedModel(access, resolvedType);
           }
         }
-        if (ObjectCodecBuilder.usesJsonParameters(method)
+        if (ObjectCodec.usesJsonParameters(method)
             || mixinSelector && method.getParameterCount() != 0) {
           registerParameterCodecs(annotations, method.getParameters());
           registerResolvedParameterTypes(ownerType, method.getParameters());
@@ -397,17 +563,16 @@ final class ForyJsonGraalVMFeature implements Feature {
       }
     }
     for (Constructor<?> constructor : type.getDeclaredConstructors()) {
-      if (annotation(annotations, constructor, JsonCreator.class) != null
-          || hasMixinSelector(annotations, constructor)) {
+      if (annotation(annotations, constructor, JsonCreator.class) != null) {
+        registerCreator(constructor);
         registerParameterCodecs(annotations, constructor.getParameters());
         registerResolvedParameterTypes(ownerType, constructor.getParameters());
-        registerUnwrappedParameters(
-            access, ownerType, annotations, constructor.getParameters());
+        registerUnwrappedParameters(access, ownerType, annotations, constructor.getParameters());
       }
     }
     for (Method method : type.getDeclaredMethods()) {
-      if (annotation(annotations, method, JsonCreator.class) != null
-          || hasMixinSelector(annotations, method)) {
+      if (annotation(annotations, method, JsonCreator.class) != null) {
+        registerCreator(method);
         registerParameterCodecs(annotations, method.getParameters());
         registerResolvedParameterTypes(ownerType, method.getParameters());
         registerUnwrappedParameters(access, ownerType, annotations, method.getParameters());
@@ -415,6 +580,37 @@ final class ForyJsonGraalVMFeature implements Feature {
     }
   }
 
+  private void registerRecord(Class<?> type) {
+    RuntimeReflection.registerAllRecordComponents(type);
+    for (RecordComponent component : type.getRecordComponents()) {
+      JsonFieldAccessor.forGetter(component.getAccessor());
+    }
+    Constructor<?> constructor = RecordUtils.getRecordConstructor(type).f0;
+    registerCreator(constructor);
+  }
+
+  private void registerCreator(Executable executable) {
+    if (processedCreators.add(executable)) {
+      RuntimeReflection.register(executable);
+      JsonCreatorInfo.creatorHandle(executable);
+    }
+  }
+
+  private static void resolveMethodAccessors(JsonMixinView annotations, Method method) {
+    int modifiers = method.getModifiers();
+    if (Modifier.isStatic(modifiers) || method.isSynthetic() || method.isBridge()) {
+      return;
+    }
+    if (method.getParameterCount() == 0 && method.getReturnType() != void.class) {
+      JsonFieldAccessor.forGetter(method);
+    } else if (method.getParameterCount() == 1 && method.getReturnType() == void.class) {
+      JsonFieldAccessor.forSetter(method);
+    }
+    if (annotation(annotations, method, JsonAnySetter.class) != null) {
+      ObjectCodec.AnyInfo.anySetterHandle(method);
+    }
+  }
+
   private void registerNestedModel(DuringAnalysisAccess access, Type type) {
     Class<?> rawType = rawType(type);
     if (rawType != null && rawType != Object.class) {
@@ -453,15 +649,13 @@ final class ForyJsonGraalVMFeature implements Feature {
     return changed;
   }
 
-  private void registerParameterCodecs(
-      JsonMixinView annotations, Parameter[] parameters) {
+  private void registerParameterCodecs(JsonMixinView annotations, Parameter[] parameters) {
     for (Parameter parameter : parameters) {
       registerCodecs(annotation(annotations, parameter, JsonCodec.class));
     }
   }
 
-  private void registerOccurrenceCodecs(
-      JsonMixinView annotations, AnnotatedElement element) {
+  private void registerOccurrenceCodecs(JsonMixinView annotations, AnnotatedElement element) {
     registerCodecs(annotation(annotations, element, JsonCodec.class));
     if (annotation(annotations, element, JsonBase64.class) != null) {
       registerCodec(Base64ByteArrayCodec.class);
@@ -555,7 +749,9 @@ final class ForyJsonGraalVMFeature implements Feature {
     }
     RuntimeReflection.register(codecClass);
     try {
-      RuntimeReflection.register(codecClass.getConstructor());
+      Constructor<?> constructor = codecClass.getConstructor();
+      RuntimeReflection.register(constructor);
+      ReflectionUtils.getCtrHandle(codecClass, new Class<?>[0]);
     } catch (NoSuchMethodException e) {
       throw new IllegalStateException(
           "JSON codec class must have a public no-argument constructor: " + codecClass.getName(),
@@ -573,7 +769,9 @@ final class ForyJsonGraalVMFeature implements Feature {
       return false;
     }
     try {
-      RuntimeReflection.register(rawType.getConstructor());
+      Constructor<?> constructor = rawType.getConstructor();
+      RuntimeReflection.register(constructor);
+      ReflectionUtils.getCtrHandle(rawType, new Class<?>[0]);
       return true;
     } catch (NoSuchMethodException ignored) {
       // CollectionCodec and MapCodec preserve the same runtime failure for a concrete container
@@ -606,7 +804,9 @@ final class ForyJsonGraalVMFeature implements Feature {
       if (type != null) {
         RuntimeReflection.register(type);
         try {
-          RuntimeReflection.register(type.getConstructor(long.class));
+          Constructor<?> constructor = type.getConstructor(long.class);
+          RuntimeReflection.register(constructor);
+          ReflectionUtils.getCtrHandle(type, long.class);
         } catch (NoSuchMethodException e) {
           throw new IllegalStateException("Missing Fory JSON SQL constructor for " + className, e);
         }
@@ -658,4 +858,46 @@ final class ForyJsonGraalVMFeature implements Feature {
     }
     return null;
   }
+
+  private static final class HostedConfiguration {
+    private final ClassLoader classLoader;
+    private final JsonSharedRegistry registry;
+    private final JsonTypeResolver resolver;
+    private final Map<Class<?>, Class<?>> mixins;
+    private final Set<Class<?>> processedModels = new LinkedHashSet<>();
+
+    private HostedConfiguration(JsonConfig config) {
+      classLoader = config.classLoader();
+      registry = JsonSharedRegistry.forHostedCodegen(config);
+      resolver = new JsonTypeResolver(registry);
+      mixins = config.mixins();
+    }
+  }
+
+  private static final class MethodSignature {
+    private final String name;
+    private final Class<?>[] parameterTypes;
+
+    private MethodSignature(Method method) {
+      name = method.getName();
+      parameterTypes = method.getParameterTypes();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (this == other) {
+        return true;
+      }
+      if (!(other instanceof MethodSignature)) {
+        return false;
+      }
+      MethodSignature that = (MethodSignature) other;
+      return name.equals(that.name) && Arrays.equals(parameterTypes, that.parameterTypes);
+    }
+
+    @Override
+    public int hashCode() {
+      return 31 * name.hashCode() + Arrays.hashCode(parameterTypes);
+    }
+  }
 }
diff --git a/java/fory-json/src/main/resources/META-INF/native-image/org.apache.fory/fory-json/native-image.properties b/java/fory-json/src/main/resources/META-INF/native-image/org.apache.fory/fory-json/native-image.properties
index b3943fab7..7feb4e41f 100644
--- a/java/fory-json/src/main/resources/META-INF/native-image/org.apache.fory/fory-json/native-image.properties
+++ b/java/fory-json/src/main/resources/META-INF/native-image/org.apache.fory/fory-json/native-image.properties
@@ -15,4 +15,18 @@
 # specific language governing permissions and limitations
 # under the License.
 
-Args=--features=org.apache.fory.json.codec.ForyJsonGraalVMFeature
+# Hosted codegen initializes only Fory JSON implementation classes. Application providers, models,
+# and custom codecs retain their own initialization policy. ScalarCodecs stays runtime initialized
+# because its temporal formatters capture JDK chronology state.
+Args=--features=org.apache.fory.json.ForyJsonGraalVMFeature \
+    --initialize-at-build-time=org.apache.fory.json.ForyJson,\
+    org.apache.fory.json.ForyJsonBuilder,\
+    org.apache.fory.json.JsonConfig,\
+    org.apache.fory.json.PropertyNamingStrategy,\
+    org.apache.fory.json.codegen,\
+    org.apache.fory.json.codec,\
+    org.apache.fory.json.meta,\
+    org.apache.fory.json.reader,\
+    org.apache.fory.json.resolver,\
+    org.apache.fory.json.writer \
+    --initialize-at-run-time=org.apache.fory.json.codec.ScalarCodecs
diff --git a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java
index dc74e0e05..2fee6272d 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java
@@ -22,6 +22,7 @@ package org.apache.fory.json;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.StringReader;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
@@ -31,20 +32,20 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Enumeration;
+import java.util.Properties;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
 
 /** Verifies the packaged Fory JSON GraalVM Feature and activation metadata. */
 public final class ForyJsonGraalVMFeatureJarVerifier {
-  private static final String FEATURE_CLASS_NAME =
-      "org.apache.fory.json.codec.ForyJsonGraalVMFeature";
+  private static final String FEATURE_CLASS_NAME = "org.apache.fory.json.ForyJsonGraalVMFeature";
   private static final String FEATURE_CLASS_FILE =
-      "org/apache/fory/json/codec/ForyJsonGraalVMFeature.class";
+      "org/apache/fory/json/ForyJsonGraalVMFeature.class";
   private static final String VERSION_17_FEATURE_CLASS =
       "META-INF/versions/17/" + FEATURE_CLASS_FILE;
   private static final String FEATURE_SOURCE_FILE =
-      "org/apache/fory/json/codec/ForyJsonGraalVMFeature.java";
+      "org/apache/fory/json/ForyJsonGraalVMFeature.java";
   private static final String VERSION_17_FEATURE_SOURCE =
       "META-INF/versions/17/" + FEATURE_SOURCE_FILE;
   private static final String NATIVE_IMAGE_PROPERTIES =
@@ -52,6 +53,28 @@ public final class ForyJsonGraalVMFeatureJarVerifier {
   private static final String FEATURE_SERVICE =
       "META-INF/services/org.graalvm.nativeimage.hosted.Feature";
   private static final String FEATURE_OPTION = "--features=" + FEATURE_CLASS_NAME;
+  private static final String BUILD_TIME_OPTION = "--initialize-at-build-time=";
+  private static final String RUNTIME_OPTION = "--initialize-at-run-time=";
+  private static final String BUILD_TIME_TARGETS =
+      "org.apache.fory.json.ForyJson,"
+          + "org.apache.fory.json.ForyJsonBuilder,"
+          + "org.apache.fory.json.JsonConfig,"
+          + "org.apache.fory.json.PropertyNamingStrategy,"
+          + "org.apache.fory.json.codegen,"
+          + "org.apache.fory.json.codec,"
+          + "org.apache.fory.json.meta,"
+          + "org.apache.fory.json.reader,"
+          + "org.apache.fory.json.resolver,"
+          + "org.apache.fory.json.writer";
+  private static final String RUNTIME_TARGETS = "org.apache.fory.json.codec.ScalarCodecs";
+  private static final String NATIVE_IMAGE_ARGS =
+      FEATURE_OPTION
+          + " "
+          + BUILD_TIME_OPTION
+          + BUILD_TIME_TARGETS
+          + " "
+          + RUNTIME_OPTION
+          + RUNTIME_TARGETS;
 
   private ForyJsonGraalVMFeatureJarVerifier() {}
 
@@ -83,9 +106,10 @@ public final class ForyJsonGraalVMFeatureJarVerifier {
       check(
           countEntries(jarFile, NATIVE_IMAGE_PROPERTIES) == 1,
           "Expected exactly one JSON native-image.properties");
-      String properties = readEntry(jarFile, NATIVE_IMAGE_PROPERTIES);
-      check(countOccurrences(properties, "--features=") == 1, "Expected one --features option");
-      check(properties.contains(FEATURE_OPTION), "Fory JSON Feature option is missing");
+      Properties properties = new Properties();
+      properties.load(new StringReader(readEntry(jarFile, NATIVE_IMAGE_PROPERTIES)));
+      String nativeImageArgs = properties.getProperty("Args");
+      check(NATIVE_IMAGE_ARGS.equals(nativeImageArgs), "Unexpected Fory JSON Native Image Args");
     }
   }
 
@@ -156,16 +180,6 @@ public final class ForyJsonGraalVMFeatureJarVerifier {
     }
   }
 
-  private static int countOccurrences(String value, String target) {
-    int count = 0;
-    int offset = 0;
-    while ((offset = value.indexOf(target, offset)) >= 0) {
-      count++;
-      offset += target.length();
-    }
-    return count;
-  }
-
   private static void check(boolean condition, String message) {
     if (!condition) {
       throw new AssertionError(message);


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@fory.apache.org
For additional commands, e-mail: commits-help@fory.apache.org