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 fb311aa06 feat(java): support Fory JSON in GraalVM native image (#3846)
fb311aa06 is described below

commit fb311aa0609480199ffaec3ca56a8001453dd0fb
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Jul 15 08:23:38 2026 +0530

    feat(java): support Fory JSON in GraalVM native image (#3846)
    
    ## Why?
    
    Fory JSON generates codecs from each `ForyJsonBuilder` configuration.
    Those configurations affect
    field discovery, naming, null handling, custom codecs, and generated
    source, so one codec generated
    during Native Image construction cannot represent every runtime builder.
    Native executables also
    cannot run the existing runtime compiler.
    
    This change supports the complete interpreted JSON codec path in GraalVM
    Native Image while keeping
    ordinary JVM code generation enabled and unchanged.
    
    ## What does this PR do?
    
    - adds the non-inherited `@JsonType` marker for reachable native JSON
    object models;
    - adds an automatically activated MR-JAR Native Image Feature that
    registers the reflection,
    constructor, unsafe-field, record, container, built-in, and
    annotation-codec metadata used by the
      interpreted path;
    - supports declaration, inherited, nested type-use, record, accessor,
    and creator `@JsonCodec`
      locations without constructing codecs at image build time;
    - disables JSON runtime code generation and asynchronous compilation
    only inside native images;
    - preserves every other `ForyJsonBuilder` option for independently
    configured runtime instances;
    - adds classpath and JPMS GraalVM acceptance coverage, MR-JAR/source-JAR
    verification, and user
      documentation.
    
    The hosted scanner reuses the existing Fory core metadata owner and the
    effective JSON-member
    classification owned by `ObjectCodecBuilder`. Hosted work does not enter
    serialization or
    deserialization hot paths, and the ordinary-JDK builder loop remains
    unchanged.
    
    ## Related issues
    
    None.
    
    ## AI Contribution Checklist
    
    ## Does this PR introduce any user-facing change?
    
    - [x] Does this PR introduce any public API change? Adds `@JsonType`.
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Validation
    
    - `cd java && ENABLE_FORY_DEBUG_OUTPUT=1 mvn -T8 -pl fory-core,fory-json
    -am test`
      - Fory core: 2,184 tests, 0 failures
      - Fory JSON: 593 tests, 0 failures
    - GraalVM 21 classpath native package/run: passed, `Fory JSON succeed`
    - GraalVM 21 JPMS native package/run: passed, `Fory JSON succeed`
    - GraalVM 25 classpath native package/run: passed, `Fory JSON succeed`
    - GraalVM 25 JPMS native package/run: passed, `Fory JSON succeed`
    - Fory JSON MR-JAR and source-JAR verification: passed
    - Java and integration Spotless checks: passed
    - Markdown Prettier and `git diff --check`: passed
    
    ## Benchmark
    
    Not applicable. The change adds Native Image hosted metadata discovery
    and native-only builder
    resolution. It does not change ordinary JVM
    serialization/deserialization hot paths, and no new
    runtime allocation is introduced there.
---
 docs/guide/java/graalvm-support.md                 |  45 ++
 docs/guide/java/index.md                           |   2 +-
 docs/guide/java/json-support.md                    |  22 +-
 integration_tests/graalvm_tests/pom.xml            |   9 +-
 .../graalvm_tests/src/main/java/module-info.java   |   5 +-
 .../org/apache/fory/graalvm/ForyJsonExample.java   | 573 +++++++++++++++++++++
 .../main/java/org/apache/fory/graalvm/Main.java    |   1 +
 java/README.md                                     |   4 +-
 .../org/apache/fory/util/record/RecordUtils.java   |  59 ++-
 .../apache/fory/platform/ForyGraalVMFeature.java   |   5 +
 .../fory-core/native-image.properties              |   1 +
 .../fory/builder/GraalvmFeatureJarVerifier.java    |   5 +
 java/fory-json/README.md                           |  26 +-
 java/fory-json/pom.xml                             | 164 +++++-
 .../java/org/apache/fory/json/ForyJsonBuilder.java |  17 +-
 .../org/apache/fory/json/annotation/JsonType.java  |  26 +-
 .../apache/fory/json/codec/ObjectCodecBuilder.java |  31 ++
 .../org/apache/fory/json/meta/JsonTypeUse.java     |   4 +-
 .../fory/json/resolver/JsonSharedRegistry.java     |   8 +-
 .../fory/json/codec/ForyJsonGraalVMFeature.java    | 384 ++++++++++++++
 java/fory-json/src/main/java9/module-info.java     |   2 +
 .../fory-json/native-image.properties              |  18 +
 .../json/ForyJsonGraalVMFeatureJarVerifier.java}   |  39 +-
 23 files changed, 1355 insertions(+), 95 deletions(-)

diff --git a/docs/guide/java/graalvm-support.md 
b/docs/guide/java/graalvm-support.md
index 72c8467bc..37095c1b5 100644
--- a/docs/guide/java/graalvm-support.md
+++ b/docs/guide/java/graalvm-support.md
@@ -44,6 +44,51 @@ shapes. Application classes still need to be registered with 
Fory before seriali
 Fory disables asynchronous serializer compilation in a native image because 
runtime just-in-time
 compilation is unavailable.
 
+## Fory JSON
+
+Fory JSON uses a separate Native Image workflow. It has no type-registration 
API and does not
+create a `ForyJson` instance or generate JSON codecs while the image is built. 
Add `@JsonType` to
+each object model that the native executable reads or writes:
+
+```java
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.annotation.JsonType;
+
+@JsonType
+public final class User {
+  public long id;
+  public String name;
+}
+
+public class JsonExample {
+  public static void main(String[] args) {
+    ForyJson json = ForyJson.builder().build();
+    User user = json.fromJson("{\"id\":1,\"name\":\"Ada\"}", User.class);
+    System.out.println(json.toJson(user));
+  }
+}
+```
+
+The `fory-json` artifact activates its Native Image Feature automatically. 
Reachable `@JsonType`
+classes gate object-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. 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 object codec. 
`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.
+
+Declaration-level, inherited, and nested type-use `@JsonCodec` annotations are 
supported. 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.
+
 ## Basic Usage
 
 ### Create Fory and Register Classes
diff --git a/docs/guide/java/index.md b/docs/guide/java/index.md
index e276e6d52..eb14921fd 100644
--- a/docs/guide/java/index.md
+++ b/docs/guide/java/index.md
@@ -254,4 +254,4 @@ ThreadSafeFory threadLocalFory = Fory.builder()
 - [Native Serialization](native-serialization.md) - Java-only serialization 
features
 - [JSON Support](json-support.md) - Complete Fory JSON user guide
 - [Static Generated Serializers](static-generated-serializers.md) - 
Annotation-processor static generated serializers for `@ForyStruct`
-- [GraalVM Support](graalvm-support.md) - Build-time serializer compilation 
for native images
+- [GraalVM Support](graalvm-support.md) - Native Image support for binary 
serialization and JSON
diff --git a/docs/guide/java/json-support.md b/docs/guide/java/json-support.md
index 343ed46e8..1956b5b3c 100644
--- a/docs/guide/java/json-support.md
+++ b/docs/guide/java/json-support.md
@@ -299,10 +299,13 @@ are rejected.
 Depth, concurrency, and retained buffer limits must be positive. 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.
+
 ## Annotations
 
 Fory JSON provides `JsonProperty`, `JsonPropertyOrder`, `JsonIgnore`, 
`JsonAnyProperty`,
-`JsonAnyGetter`, `JsonAnySetter`, `JsonCreator`, `JsonCodec`, and 
`JsonSubTypes` under
+`JsonAnyGetter`, `JsonAnySetter`, `JsonCreator`, `JsonCodec`, `JsonSubTypes`, 
and `JsonType` under
 `org.apache.fory.json.annotation`. They are not Jackson, Gson, or Fory 
binary-protocol annotations.
 
 ```java
@@ -318,8 +321,13 @@ import org.apache.fory.json.annotation.JsonIgnore;
 import org.apache.fory.json.annotation.JsonProperty;
 import org.apache.fory.json.annotation.JsonPropertyOrder;
 import org.apache.fory.json.annotation.JsonSubTypes;
+import org.apache.fory.json.annotation.JsonType;
 ```
 
+`JsonType` marks a reachable object model for GraalVM Native Image metadata. 
It has no effect on
+ordinary JVM JSON behavior and is not inherited. See [GraalVM 
Support](graalvm-support.md) for the
+complete native-image workflow.
+
 ### `JsonProperty`
 
 An annotation on a field, getter, or setter configures the complete merged 
logical property:
@@ -628,7 +636,8 @@ parent does not admit descendants. The annotation is read 
from the declared base
 inherited from another annotated base. Null is plain JSON null unless codec 
precedence selects a
 custom complete-value codec for the declared base, replacing the annotation. 
Readers accept only
 the configured shape, so changing inclusion is a wire-format change. At 
GraalVM native-image
-runtime, use class literals instead of `className`.
+runtime, annotate the base with `JsonType` and use class literals instead of 
`className`. Listed
+class-literal subtypes are registered automatically.
 
 ## Custom codecs
 
@@ -925,10 +934,11 @@ succeed. Returning a plain parent while decoding the 
child fails with `ForyJsonE
 names the target type, codec class, declaring type, and actual returned type. 
The actual result,
 not the parent's `final` status or the codec's generic signature, determines 
validity.
 
-`@JsonCodec` is supported on ordinary JVMs. Android and GraalVM native image 
ignore both declaration
-and type-use forms, avoiding partial behavior when nested runtime type 
metadata is unavailable. Use
-exact `registerCodec` registration there. Native-image reflection 
configuration is not a supported
-way to enable part of this annotation feature.
+`@JsonCodec` is supported on ordinary JVMs and GraalVM native images, 
including inherited
+declarations and nested type uses. Native models must follow the `JsonType` 
workflow described in
+[GraalVM Support](graalvm-support.md), which registers the annotation codec's 
public no-argument
+constructor. Android ignores declaration and type-use forms; use exact 
`registerCodec`
+registration there.
 
 ## Type validation and untrusted input
 
diff --git a/integration_tests/graalvm_tests/pom.xml 
b/integration_tests/graalvm_tests/pom.xml
index ce204a5a1..13acb89b9 100644
--- a/integration_tests/graalvm_tests/pom.xml
+++ b/integration_tests/graalvm_tests/pom.xml
@@ -62,6 +62,11 @@
       <artifactId>fory-core</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.fory</groupId>
+      <artifactId>fory-json</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 
   <build>
@@ -254,7 +259,7 @@
                 </goals>
                 <configuration>
                   <excludeTransitive>true</excludeTransitive>
-                  <includeArtifactIds>fory-core</includeArtifactIds>
+                  <includeArtifactIds>fory-core,fory-json</includeArtifactIds>
                   <includeScope>runtime</includeScope>
                   
<outputDirectory>${project.build.directory}/module-path</outputDirectory>
                 </configuration>
@@ -280,7 +285,7 @@
                     <argument>-o</argument>
                     
<argument>${project.build.directory}/${moduleImageName}</argument>
                     <argument>--module-path</argument>
-                    
<argument>${project.build.outputDirectory}${path.separator}${project.build.directory}/module-path/fory-core-${project.version}.jar</argument>
+                    
<argument>${project.build.outputDirectory}${path.separator}${project.build.directory}/module-path/fory-core-${project.version}.jar${path.separator}${project.build.directory}/module-path/fory-json-${project.version}.jar</argument>
                     <argument>--module</argument>
                     <argument>${mainModule}/${mainClass}</argument>
                   </arguments>
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 d3e59bb81..e514b7beb 100644
--- a/integration_tests/graalvm_tests/src/main/java/module-info.java
+++ b/integration_tests/graalvm_tests/src/main/java/module-info.java
@@ -19,9 +19,10 @@
 
 module org.apache.fory.graalvm.tests {
   requires org.apache.fory.core;
+  requires org.apache.fory.json;
+  requires java.sql;
 
-  // Fory-generated codecs are defined by the existing codegen class loader, 
so they access the
-  // registered test models from its unnamed module.
+  // Fory-generated codecs and annotation codecs access the test models from 
library modules.
   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
new file mode 100644
index 000000000..926608d5d
--- /dev/null
+++ 
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
@@ -0,0 +1,573 @@
+/*
+ * 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;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.PropertyNamingStrategy;
+import org.apache.fory.json.annotation.JsonAnyProperty;
+import org.apache.fory.json.annotation.JsonCodec;
+import org.apache.fory.json.annotation.JsonCreator;
+import org.apache.fory.json.annotation.JsonProperty;
+import org.apache.fory.json.annotation.JsonSubTypes;
+import org.apache.fory.json.annotation.JsonType;
+import org.apache.fory.json.codec.JsonValueCodec;
+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.util.Preconditions;
+
+/** Native-image acceptance coverage for the complete interpreted Fory JSON 
path. */
+public final class ForyJsonExample {
+  private ForyJsonExample() {}
+
+  public static void main(String[] args) {
+    testModels();
+    testConfigurations();
+    testCodecs();
+    testSubtypes();
+    testContainerRoots();
+    testGenericProperties();
+    testBigDecimal();
+    testSqlTypes();
+    System.out.println("Fory JSON succeed");
+  }
+
+  private static void testModels() {
+    ForyJson json = ForyJson.builder().build();
+    Model value = new Model();
+    value.child = new Child(1, "first");
+    value.children = List.of(new Child(2, "second"));
+    value.childrenByName = Map.of("third", new Child(3, "third"));
+    value.concreteChildren = new ArrayList<>(List.of(new Child(11, 
"concrete")));
+    value.concreteChildrenByName = new HashMap<>();
+    value.concreteChildrenByName.put("map", new Child(12, "concrete-map"));
+    value.childArray = new Child[] {new Child(4, "fourth")};
+    value.status = Status.ACTIVE;
+    value.bean = new Bean("interface");
+    value.record = new DataRecord(5, "record");
+    value.creator = new CreatorValue(6, "creator");
+    value.factory = FactoryValue.create(7, "factory");
+    value.extra.put("dynamic", 8);
+
+    byte[] bytes = json.toJsonBytes(value);
+    Model decoded = json.fromJson(bytes, Model.class);
+    Preconditions.checkArgument(decoded.inheritedId() == 10);
+    Preconditions.checkArgument(decoded.child.id == 1);
+    Preconditions.checkArgument(decoded.children.get(0).name.equals("second"));
+    Preconditions.checkArgument(decoded.childrenByName.get("third").id == 3);
+    Preconditions.checkArgument(decoded.concreteChildren.get(0).id == 11);
+    Preconditions.checkArgument(decoded.concreteChildrenByName.get("map").id 
== 12);
+    Preconditions.checkArgument(decoded.childArray[0].name.equals("fourth"));
+    Preconditions.checkArgument(decoded.status == Status.ACTIVE);
+    
Preconditions.checkArgument(decoded.bean.getDisplayName().equals("interface"));
+    Preconditions.checkArgument(decoded.record.equals(new DataRecord(5, 
"record")));
+    Preconditions.checkArgument(decoded.creator.name.equals("creator"));
+    Preconditions.checkArgument(decoded.factory.id == 7);
+    Preconditions.checkArgument(decoded.extra.containsKey("dynamic"));
+  }
+
+  private static void testConfigurations() {
+    ConfigValue value = new ConfigValue();
+    value.camelName = "configured";
+    String defaults = ForyJson.builder().build().toJson(value);
+    Preconditions.checkArgument(defaults.contains("\"camelName\""));
+    Preconditions.checkArgument(!defaults.contains("nullValue"));
+
+    ForyJson configured =
+        ForyJson.builder()
+            .withFieldMode(true)
+            .writeNullFields(true)
+            .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
+            .build();
+    String snakeCase = configured.toJson(value);
+    Preconditions.checkArgument(snakeCase.contains("\"camel_name\""));
+    Preconditions.checkArgument(snakeCase.contains("\"null_value\":null"));
+    ConfigValue decoded = configured.fromJson(snakeCase, ConfigValue.class);
+    Preconditions.checkArgument(decoded.camelName.equals("configured"));
+  }
+
+  private static void testCodecs() {
+    ForyJson json = ForyJson.builder().build();
+    CodecModel value = new CodecModel();
+    value.direct = new DirectValue("direct");
+    value.inherited = new InheritedValue("inherited");
+    value.nested = List.of(new TypeUseValue("nested"));
+    value.array = new TypeUseValue[] {new TypeUseValue("array")};
+    value.mapped = Map.of("key", new TypeUseValue("mapped"));
+    value.property = new TypeUseValue("property");
+    value.record = new CodecRecord(new TypeUseValue("record"));
+    value.creator = new CodecCreator(new TypeUseValue("creator"));
+    value.factory = CodecFactory.create(new TypeUseValue("factory"));
+
+    String stringJson = json.toJson(value);
+    Preconditions.checkArgument(stringJson.contains("string:direct"));
+    Preconditions.checkArgument(stringJson.contains("string:inherited"));
+    Preconditions.checkArgument(stringJson.contains("string:nested"));
+    Preconditions.checkArgument(stringJson.contains("string:array"));
+    Preconditions.checkArgument(stringJson.contains("string:mapped"));
+    String utf8Json = new String(json.toJsonBytes(value), 
StandardCharsets.UTF_8);
+    Preconditions.checkArgument(utf8Json.contains("utf8:direct"));
+    Preconditions.checkArgument(utf8Json.contains("utf8:property"));
+
+    DirectValue stringValue = json.fromJson("\"value\"", DirectValue.class);
+    DirectValue utf16 = json.fromJson("\"\u4f60\"", DirectValue.class);
+    DirectValue utf8 =
+        json.fromJson("\"value\"".getBytes(StandardCharsets.UTF_8), 
DirectValue.class);
+    checkStringRead(stringValue.text, "value");
+    Preconditions.checkArgument(utf16.text.equals("utf16:\u4f60"));
+    Preconditions.checkArgument(utf8.text.equals("utf8:value"));
+
+    CodecModel decoded = json.fromJson(stringJson, CodecModel.class);
+    checkStringRead(decoded.direct.text, "string:direct");
+    checkStringRead(decoded.inherited.text, "string:inherited");
+    checkStringRead(decoded.nested.get(0).text, "string:nested");
+    checkStringRead(decoded.array[0].text, "string:array");
+    checkStringRead(decoded.mapped.get("key").text, "string:mapped");
+    checkStringRead(decoded.property.text, "string:property");
+    checkStringRead(decoded.record.value.text, "string:record");
+    checkStringRead(decoded.creator.value.text, "string:creator");
+    checkStringRead(decoded.factory.value.text, "string:factory");
+  }
+
+  private static void checkStringRead(String actual, String value) {
+    Preconditions.checkArgument(actual.equals("latin:" + value) || 
actual.equals("utf16:" + value));
+  }
+
+  private static void testSubtypes() {
+    ForyJson json = ForyJson.builder().build();
+    Shape value = new Circle(9);
+    String encoded = json.toJson(value, Shape.class);
+    Shape decoded = json.fromJson(encoded, Shape.class);
+    Preconditions.checkArgument(decoded instanceof Circle);
+    Preconditions.checkArgument(((Circle) decoded).radius == 9);
+  }
+
+  private static void testContainerRoots() {
+    ForyJson json = ForyJson.builder().build();
+    StringList list = json.fromJson("[\"first\",\"second\"]", 
StringList.class);
+    Preconditions.checkArgument(list.equals(List.of("first", "second")));
+    StringMap map = json.fromJson("{\"key\":\"value\"}", StringMap.class);
+    Preconditions.checkArgument(map.equals(Map.of("key", "value")));
+  }
+
+  private static void testGenericProperties() {
+    ForyJson json = ForyJson.builder().build();
+    GenericModel value =
+        json.fromJson("{\"values\":[{\"id\":13,\"name\":\"generic\"}]}", 
GenericModel.class);
+    Object values = value.getValues();
+    Preconditions.checkArgument(
+        values.getClass().getName().equals(ForyJsonExample.class.getName() + 
"$ChildList"));
+    Child child = (Child) ((List<?>) values).get(0);
+    Preconditions.checkArgument(child.id == 13);
+    Preconditions.checkArgument(child.name.equals("generic"));
+  }
+
+  private static void testBigDecimal() {
+    ForyJson json = ForyJson.builder().build();
+    BigDecimalHolder value = new BigDecimalHolder();
+    value.value = new BigDecimalSubtype("12345678901234567890.125");
+    String expected = "{\"value\":12345678901234567890.125}";
+    Preconditions.checkArgument(json.toJson(value).equals(expected));
+    Preconditions.checkArgument(
+        new String(json.toJsonBytes(value), 
StandardCharsets.UTF_8).equals(expected));
+  }
+
+  private static void testSqlTypes() {
+    ForyJson json = ForyJson.builder().build();
+    SqlValues value = new SqlValues();
+    value.date = new Date(1_000L);
+    value.time = new Time(2_000L);
+    value.timestamp = new Timestamp(3_000L);
+    SqlValues decoded = json.fromJson(json.toJsonBytes(value), 
SqlValues.class);
+    Preconditions.checkArgument(decoded.date.getTime() == 1_000L);
+    Preconditions.checkArgument(decoded.time.getTime() == 2_000L);
+    Preconditions.checkArgument(decoded.timestamp.getTime() == 3_000L);
+  }
+
+  public static class Parent {
+    private int inheritedId = 10;
+
+    int inheritedId() {
+      return inheritedId;
+    }
+  }
+
+  public static final class StringList extends ArrayList<String> {
+    public StringList() {}
+  }
+
+  public static final class StringMap extends HashMap<String, String> {
+    public StringMap() {}
+  }
+
+  public abstract static class GenericProperty<T> {
+    private Object value;
+
+    @SuppressWarnings("unchecked")
+    public T getValues() {
+      return (T) value;
+    }
+
+    public void setValues(T value) {
+      this.value = value;
+    }
+  }
+
+  @JsonType
+  public static final class GenericModel extends GenericProperty<ChildList> {}
+
+  public static final class ChildList extends ArrayList<Child> {
+    public ChildList() {}
+  }
+
+  @JsonType
+  public static final class BigDecimalHolder {
+    public BigDecimal value;
+  }
+
+  private static final class BigDecimalSubtype extends BigDecimal {
+    private BigDecimalSubtype(String value) {
+      super(value);
+    }
+
+    @Override
+    public String toString() {
+      throw new AssertionError("BigDecimal subtype toString must not be 
invoked");
+    }
+
+    @Override
+    public BigInteger unscaledValue() {
+      throw new AssertionError("BigDecimal subtype unscaledValue must not be 
invoked");
+    }
+
+    @Override
+    public int scale() {
+      throw new AssertionError("BigDecimal subtype scale must not be invoked");
+    }
+
+    @Override
+    public BigDecimal negate() {
+      throw new AssertionError("BigDecimal subtype negate must not be 
invoked");
+    }
+  }
+
+  @JsonType
+  public static final class Model extends Parent {
+    public Child child;
+    public List<Child> children;
+    public Map<String, Child> childrenByName;
+    public ArrayList<Child> concreteChildren;
+    public HashMap<String, Child> concreteChildrenByName;
+    public Child[] childArray;
+    public Status status;
+    public Bean bean;
+    public DataRecord record;
+    public CreatorValue creator;
+    public FactoryValue factory;
+
+    @JsonAnyProperty public Map<String, Object> extra = new LinkedHashMap<>();
+  }
+
+  @JsonType
+  public static final class Child {
+    public int id;
+    public String name;
+
+    public Child() {}
+
+    Child(int id, String name) {
+      this.id = id;
+      this.name = name;
+    }
+  }
+
+  @JsonType
+  public enum Status {
+    ACTIVE,
+    INACTIVE
+  }
+
+  public interface NamedBean {
+    String getDisplayName();
+
+    void setDisplayName(String value);
+  }
+
+  @JsonType
+  public static final class Bean implements NamedBean {
+    private String displayName;
+
+    public Bean() {}
+
+    Bean(String displayName) {
+      this.displayName = displayName;
+    }
+
+    @Override
+    public String getDisplayName() {
+      return displayName;
+    }
+
+    @Override
+    public void setDisplayName(String value) {
+      displayName = value;
+    }
+  }
+
+  @JsonType
+  public record DataRecord(int id, String name) {}
+
+  @JsonType
+  public static final class CreatorValue {
+    public final int id;
+    public final String name;
+
+    @JsonCreator({"id", "name"})
+    public CreatorValue(int id, String name) {
+      this.id = id;
+      this.name = name;
+    }
+  }
+
+  @JsonType
+  public static final class FactoryValue {
+    public final int id;
+    public final String name;
+
+    private FactoryValue(int id, String name) {
+      this.id = id;
+      this.name = name;
+    }
+
+    @JsonCreator({"id", "name"})
+    public static FactoryValue create(int id, String name) {
+      return new FactoryValue(id, name);
+    }
+  }
+
+  @JsonType
+  public static final class ConfigValue {
+    public String camelName;
+    public String nullValue;
+
+    public @JsonCodec(IgnoredCodec.class) TypeUseValue ignoredCodecMethod() {
+      throw new AssertionError("Non-property methods must not participate in 
JSON metadata");
+    }
+  }
+
+  @JsonType
+  public static final class CodecModel {
+    public DirectValue direct;
+    public InheritedValue inherited;
+    public List<@JsonCodec(TypeUseCodec.class) TypeUseValue> nested = new 
ArrayList<>();
+    public @JsonCodec(TypeUseCodec.class) TypeUseValue[] array;
+    public Map<String, @JsonCodec(TypeUseCodec.class) TypeUseValue> mapped;
+    private TypeUseValue property;
+    public CodecRecord record;
+    public CodecCreator creator;
+    public CodecFactory factory;
+
+    public @JsonCodec(TypeUseCodec.class) TypeUseValue getProperty() {
+      return property;
+    }
+
+    public void setProperty(@JsonCodec(TypeUseCodec.class) TypeUseValue 
property) {
+      this.property = property;
+    }
+  }
+
+  @JsonCodec(DirectCodec.class)
+  public static final class DirectValue implements TextValue {
+    private final String text;
+
+    DirectValue(String text) {
+      this.text = text;
+    }
+
+    @Override
+    public String text() {
+      return text;
+    }
+  }
+
+  @JsonCodec(InheritedCodec.class)
+  public interface InheritedText extends TextValue {}
+
+  public static final class InheritedValue implements InheritedText {
+    private final String text;
+
+    InheritedValue(String text) {
+      this.text = text;
+    }
+
+    @Override
+    public String text() {
+      return text;
+    }
+  }
+
+  public static final class TypeUseValue implements TextValue {
+    private final String text;
+
+    TypeUseValue(String text) {
+      this.text = text;
+    }
+
+    @Override
+    public String text() {
+      return text;
+    }
+  }
+
+  @JsonType
+  public record CodecRecord(@JsonCodec(TypeUseCodec.class) TypeUseValue value) 
{}
+
+  @JsonType
+  public static final class CodecCreator {
+    public final TypeUseValue value;
+
+    @JsonCreator
+    public CodecCreator(@JsonProperty("value") @JsonCodec(TypeUseCodec.class) 
TypeUseValue value) {
+      this.value = value;
+    }
+  }
+
+  @JsonType
+  public static final class CodecFactory {
+    public final TypeUseValue value;
+
+    private CodecFactory(TypeUseValue value) {
+      this.value = value;
+    }
+
+    @JsonCreator
+    public static CodecFactory create(
+        @JsonProperty("value") @JsonCodec(TypeUseCodec.class) TypeUseValue 
value) {
+      return new CodecFactory(value);
+    }
+  }
+
+  public interface TextValue {
+    String text();
+  }
+
+  public abstract static class TextCodec<T extends TextValue> implements 
JsonValueCodec<T> {
+    public TextCodec() {}
+
+    protected abstract T create(String text);
+
+    @Override
+    public void writeString(StringJsonWriter writer, T value) {
+      writer.writeString(value == null ? null : "string:" + value.text());
+    }
+
+    @Override
+    public void writeUtf8(Utf8JsonWriter writer, T value) {
+      writer.writeString(value == null ? null : "utf8:" + value.text());
+    }
+
+    @Override
+    public T readLatin1(Latin1JsonReader reader) {
+      return reader.tryReadNullToken() ? null : create("latin:" + 
reader.readString());
+    }
+
+    @Override
+    public T readUtf16(Utf16JsonReader reader) {
+      return reader.tryReadNullToken() ? null : create("utf16:" + 
reader.readString());
+    }
+
+    @Override
+    public T readUtf8(Utf8JsonReader reader) {
+      return reader.tryReadNullToken() ? null : create("utf8:" + 
reader.readString());
+    }
+  }
+
+  public static final class DirectCodec extends TextCodec<DirectValue> {
+    public DirectCodec() {}
+
+    @Override
+    protected DirectValue create(String text) {
+      return new DirectValue(text);
+    }
+  }
+
+  public static final class InheritedCodec extends TextCodec<InheritedValue> {
+    public InheritedCodec() {}
+
+    @Override
+    protected InheritedValue create(String text) {
+      return new InheritedValue(text);
+    }
+  }
+
+  public static final class TypeUseCodec extends TextCodec<TypeUseValue> {
+    public TypeUseCodec() {}
+
+    @Override
+    protected TypeUseValue create(String text) {
+      return new TypeUseValue(text);
+    }
+  }
+
+  public static final class IgnoredCodec extends TextCodec<TypeUseValue> {
+    private IgnoredCodec(String ignored) {}
+
+    @Override
+    protected TypeUseValue create(String text) {
+      return new TypeUseValue(text);
+    }
+  }
+
+  @JsonType
+  @JsonSubTypes(
+      property = "kind",
+      value = {@JsonSubTypes.Type(value = Circle.class, name = "circle")})
+  public interface Shape {}
+
+  public static final class Circle implements Shape {
+    public int radius;
+
+    public Circle() {}
+
+    Circle(int radius) {
+      this.radius = radius;
+    }
+  }
+
+  @JsonType
+  public static final class SqlValues {
+    public Date date;
+    public Time time;
+    public Timestamp timestamp;
+  }
+}
diff --git 
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
 
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
index f84eeb50b..14e290f31 100644
--- 
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
+++ 
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
@@ -46,5 +46,6 @@ public class Main {
     ExceptionExample.main(args);
     AbstractClassExample.main(args);
     FeatureTestExample.main(args);
+    ForyJsonExample.main(args);
   }
 }
diff --git a/java/README.md b/java/README.md
index f5955a51e..80fdd66ca 100644
--- a/java/README.md
+++ b/java/README.md
@@ -366,7 +366,9 @@ java --add-modules=jdk.incubator.vector ...
 
 ### GraalVM Native Image
 
-Fory supports GraalVM native image through code generation, eliminating the 
need for reflection configuration. Build your native image as follows:
+Fory supports GraalVM Native Image without application reflection 
configuration. Binary
+serialization generates serializers while the image is built; Fory JSON 
registers reachable
+`@JsonType` models and uses its interpreted codec path. Build your native 
image as follows:
 
 ```bash
 # Generate serializers at build time
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/util/record/RecordUtils.java 
b/java/fory-core/src/main/java/org/apache/fory/util/record/RecordUtils.java
index 215a92152..a59641449 100644
--- a/java/fory-core/src/main/java/org/apache/fory/util/record/RecordUtils.java
+++ b/java/fory-core/src/main/java/org/apache/fory/util/record/RecordUtils.java
@@ -31,10 +31,12 @@ import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import org.apache.fory.annotation.Internal;
 import org.apache.fory.collection.ClassValueCache;
 import org.apache.fory.collection.Tuple2;
 import org.apache.fory.exception.ForyException;
 import org.apache.fory.platform.AndroidSupport;
+import org.apache.fory.platform.GraalvmSupport;
 import org.apache.fory.platform.internal._JDKAccess;
 
 /** Utils for java.lang.Record. */
@@ -100,6 +102,12 @@ public class RecordUtils {
   private static final ClassValueCache<Tuple2<Constructor, MethodHandle>> 
ctrCache =
       ClassValueCache.newClassKeyCache(32);
 
+  // This holder must remain in fory-core's Native Image build-time 
initialization list. Its cache
+  // stores getter functions created during analysis because Native Image 
cannot create them later.
+  private static final class NativeImageRecordGetters {
+    private static final ClassValueCache<Object[]> CACHE = 
ClassValueCache.newClassKeyCache(32);
+  }
+
   /**
    * Returns {@code true} if and only if this class is a record class.
    *
@@ -134,9 +142,26 @@ public class RecordUtils {
     if (GET_RECORD_COMPONENTS == null) {
       return null;
     }
+    if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && 
GraalvmSupport.isGraalBuildTime()) {
+      // Annotated type values can be JDK annotation proxies, which Native 
Image must not persist in
+      // the image heap. Keep only the runtime-required getter functions and 
let build-time codegen
+      // consume transient component metadata.
+      prepareRecordComponentGetters(cls);
+      return getRecordComponentsUncached(cls);
+    }
     return recordComponentsCache.get(cls, () -> 
getRecordComponentsUncached(cls));
   }
 
+  /** Prepares record getter functions during Native Image hosted analysis. */
+  @Internal
+  public static void prepareRecordComponentGetters(Class<?> cls) {
+    if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE
+        && GET_RECORD_COMPONENTS != null
+        && !AndroidSupport.IS_ANDROID) {
+      NativeImageRecordGetters.CACHE.get(cls, () -> 
buildRecordComponentGetters(cls));
+    }
+  }
+
   /** Returns the record canonical constructor. */
   public static Tuple2<Constructor, MethodHandle> 
getRecordConstructor(Class<?> cls) {
     return ctrCache.get(cls, () -> getRecordConstructorUncached(cls));
@@ -152,9 +177,15 @@ public class RecordUtils {
 
   private static RecordComponent[] getRecordComponentsUncached(Class<?> type) {
     try {
-      MethodHandles.Lookup lookup =
-          AndroidSupport.IS_ANDROID ? null : _JDKAccess._trustedLookup(type);
       Object[] components = (Object[]) GET_RECORD_COMPONENTS.invoke(type);
+      Object[] preparedGetters =
+          GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE
+              ? NativeImageRecordGetters.CACHE.getIfPresent(type)
+              : null;
+      MethodHandles.Lookup lookup =
+          AndroidSupport.IS_ANDROID || preparedGetters != null
+              ? null
+              : _JDKAccess._trustedLookup(type);
       RecordComponent[] recordComponents = new 
RecordComponent[components.length];
       for (int i = 0; i < components.length; i++) {
         Object component = components[i];
@@ -168,8 +199,12 @@ public class RecordUtils {
             throw new ForyException("Failed to make record accessor 
accessible: " + accessor, e);
           }
         } else {
-          MethodHandle handle = lookup.unreflect(accessor);
-          getter = _JDKAccess.makeGetterFunction(lookup, handle, fieldType);
+          if (preparedGetters == null) {
+            MethodHandle handle = lookup.unreflect(accessor);
+            getter = _JDKAccess.makeGetterFunction(lookup, handle, fieldType);
+          } else {
+            getter = preparedGetters[i];
+          }
         }
         recordComponents[i] =
             new RecordComponent(
@@ -187,6 +222,22 @@ public class RecordUtils {
     }
   }
 
+  private static Object[] buildRecordComponentGetters(Class<?> type) {
+    try {
+      MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(type);
+      Object[] components = (Object[]) GET_RECORD_COMPONENTS.invoke(type);
+      Object[] getters = new Object[components.length];
+      for (int i = 0; i < components.length; i++) {
+        Method accessor = (Method) GET_ACCESSOR.invoke(components[i]);
+        Class<?> fieldType = (Class<?>) GET_TYPE.invoke(components[i]);
+        getters[i] = _JDKAccess.makeGetterFunction(lookup, 
lookup.unreflect(accessor), fieldType);
+      }
+      return getters;
+    } catch (IllegalAccessException | InvocationTargetException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   private static Tuple2<Constructor, MethodHandle> 
getRecordConstructorUncached(Class<?> type) {
     RecordComponent[] components = RecordUtils.getRecordComponents(type);
     if (components == null) {
diff --git 
a/java/fory-core/src/main/java17/org/apache/fory/platform/ForyGraalVMFeature.java
 
b/java/fory-core/src/main/java17/org/apache/fory/platform/ForyGraalVMFeature.java
index ba0edfe15..b9b77d06e 100644
--- 
a/java/fory-core/src/main/java17/org/apache/fory/platform/ForyGraalVMFeature.java
+++ 
b/java/fory-core/src/main/java17/org/apache/fory/platform/ForyGraalVMFeature.java
@@ -111,6 +111,11 @@ final class ForyGraalVMFeature implements Feature {
 
     if (RecordUtils.isRecord(clazz)) {
       RuntimeReflection.registerAllRecordComponents(clazz);
+      // Native Image cannot spin the hidden getter classes used by 
RecordUtils at runtime. Build
+      // only those getters here; annotated type proxies must remain runtime 
metadata instead of
+      // becoming objects in the image heap.
+      RecordUtils.prepareRecordComponentGetters(clazz);
+      RecordUtils.getRecordConstructor(clazz);
     } else if (GraalvmSupport.needReflectionRegisterForCreation(clazz)) {
       RuntimeReflection.registerForReflectiveInstantiation(clazz);
     }
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 d20a33f48..f5e5f2c88 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
@@ -542,6 +542,7 @@ Args=--features=org.apache.fory.platform.ForyGraalVMFeature 
\
     org.apache.fory.util.record.RecordUtils$1,\
     org.apache.fory.util.record.RecordUtils$2,\
     org.apache.fory.util.record.RecordUtils$3,\
+    org.apache.fory.util.record.RecordUtils$NativeImageRecordGetters,\
     org.apache.fory.util.record.RecordUtils,\
     org.apache.fory.serializer.PlatformStringUtils$StringCoderField,\
     org.apache.fory.platform.internal._JDKAccess,\
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
 
b/java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
index 5683ad4c0..baad6d1d2 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
+++ 
b/java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
@@ -52,6 +52,8 @@ public final class GraalvmFeatureJarVerifier {
       "META-INF/services/org.graalvm.nativeimage.hosted.Feature";
   private static final String FEATURE_OPTION = "--features=" + 
FEATURE_CLASS_NAME;
   private static final String INITIALIZATION_OPTION = 
"--initialize-at-build-time=";
+  private static final String RECORD_GETTERS =
+      "org.apache.fory.util.record.RecordUtils$NativeImageRecordGetters";
 
   private GraalvmFeatureJarVerifier() {}
 
@@ -92,6 +94,9 @@ public final class GraalvmFeatureJarVerifier {
       check(
           properties.contains(INITIALIZATION_OPTION),
           "Build-time initialization option is missing");
+      check(
+          properties.contains(RECORD_GETTERS),
+          "Native Image record getter cache must initialize at build time");
     }
   }
 
diff --git a/java/fory-json/README.md b/java/fory-json/README.md
index 9f19704be..4874abf8d 100644
--- a/java/fory-json/README.md
+++ b/java/fory-json/README.md
@@ -356,11 +356,18 @@ operation. Apply request/body size limits at the 
transport boundary when parsing
 
 Builder mutation after `build()` does not modify an existing `ForyJson` 
runtime.
 
+In a GraalVM native image, runtime code generation and asynchronous 
compilation are automatically
+disabled. Every other builder option keeps the behavior described above.
+
 ## JSON annotations
 
-Fory JSON defines nine annotations in `org.apache.fory.json.annotation`, 
including `JsonCodec` for
-complete-value codec selection. They are Fory JSON APIs, not Jackson, Gson, or 
Fory binary-protocol
-compatibility annotations.
+Fory JSON defines ten annotations in `org.apache.fory.json.annotation`, 
including `JsonCodec` for
+complete-value codec selection and `JsonType` for GraalVM Native Image model 
metadata. They are Fory
+JSON APIs, not Jackson, Gson, or Fory binary-protocol compatibility 
annotations.
+
+`JsonType` has no effect on ordinary JVM JSON behavior and is not inherited. 
Add it to every
+reachable object model used by a native executable. See the
+[GraalVM guide](../../docs/guide/java/graalvm-support.md) for the complete 
workflow.
 
 ### `JsonProperty`
 
@@ -745,7 +752,8 @@ is not inherited from another annotated interface or 
abstract class. Readers acc
 configured inclusion; changing inclusion is a wire-format change and there is 
no dual-read
 fallback.
 
-At GraalVM native-image runtime, use class-literal entries rather than 
`className` entries.
+At GraalVM native-image runtime, annotate the base with `JsonType` and use 
class-literal entries
+rather than `className` entries. Listed class-literal subtypes are registered 
automatically.
 
 ## Custom codecs
 
@@ -1070,11 +1078,11 @@ plain `Base` fails with `ForyJsonException` containing 
the target type, codec cl
 and actual returned class. Fory validates the actual result rather than 
rejecting inheritance based
 on the codec's generic signature.
 
-`@JsonCodec` declaration and type-use discovery is supported on ordinary JVMs. 
Android and GraalVM
-native image ignore all `@JsonCodec` sources so that declaration defaults 
cannot work while nested
-type-use metadata silently disappears. Use exact `registerCodec(Target.class, 
instance)`
-registration for both environments. Native-image reflection configuration is 
not a supported way
-to enable only part of the annotation feature.
+`@JsonCodec` declaration and type-use discovery is supported on ordinary JVMs 
and GraalVM native
+images, including inherited declarations and nested type uses. Native object 
models must follow the
+`JsonType` workflow in the [GraalVM 
guide](../../docs/guide/java/graalvm-support.md), which registers
+the annotation codec's public no-argument constructor. Android ignores 
annotation codec sources;
+use exact `registerCodec(Target.class, instance)` registration there.
 
 ## Type validation and untrusted input
 
diff --git a/java/fory-json/pom.xml b/java/fory-json/pom.xml
index 30c1c9e70..69dac5e7e 100644
--- a/java/fory-json/pom.xml
+++ b/java/fory-json/pom.xml
@@ -38,6 +38,7 @@
   <properties>
     <maven.compiler.source>8</maven.compiler.source>
     <maven.compiler.target>8</maven.compiler.target>
+    <maven.source.skip>false</maven.source.skip>
     <fory.java.rootdir>${basedir}/..</fory.java.rootdir>
     
<fory.json.mr.classes>${project.build.directory}/multi-release-classes</fory.json.mr.classes>
     
<fory.json.jdk25.test.classes>${project.build.directory}/jdk25-test-classes</fory.json.jdk25.test.classes>
@@ -86,6 +87,20 @@
       <activation>
         <jdk>[9,)</jdk>
       </activation>
+      <dependencies>
+        <dependency>
+          <groupId>org.graalvm.sdk</groupId>
+          <artifactId>graal-sdk</artifactId>
+          <version>23.1.0</version>
+          <scope>provided</scope>
+        </dependency>
+        <dependency>
+          <groupId>org.graalvm.sdk</groupId>
+          <artifactId>nativeimage</artifactId>
+          <version>23.1.0</version>
+          <scope>provided</scope>
+        </dependency>
+      </dependencies>
       <build>
         <plugins>
           <plugin>
@@ -162,9 +177,9 @@
       </build>
     </profile>
     <profile>
-      <id>jdk25-multi-release</id>
+      <id>graalvm-feature-java17</id>
       <activation>
-        <jdk>[25,)</jdk>
+        <jdk>[17,)</jdk>
       </activation>
       <build>
         <plugins>
@@ -181,6 +196,127 @@
               </execution>
             </executions>
           </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-antrun-plugin</artifactId>
+            <version>3.1.0</version>
+            <executions>
+              <execution>
+                <id>compile-java17-feature</id>
+                <phase>process-classes</phase>
+                <goals>
+                  <goal>run</goal>
+                </goals>
+                <configuration>
+                  <target>
+                    <delete dir="${fory.json.mr.classes}/17"/>
+                    <mkdir dir="${fory.json.mr.classes}/17"/>
+                    <javac srcdir="${project.basedir}/src/main/java17"
+                           destdir="${fory.json.mr.classes}/17"
+                           includeantruntime="false"
+                           fork="true"
+                           executable="${java.home}/bin/javac"
+                           debug="true">
+                      <include name="org/**/*.java"/>
+                      <classpath>
+                        <path refid="maven.compile.classpath"/>
+                        <pathelement 
location="${project.build.outputDirectory}"/>
+                      </classpath>
+                      <compilerarg value="--release"/>
+                      <compilerarg value="17"/>
+                      <compilerarg value="-sourcepath"/>
+                      <compilerarg value=""/>
+                    </javac>
+                  </target>
+                </configuration>
+              </execution>
+              <execution>
+                <id>inject-java17-feature</id>
+                <phase>package</phase>
+                <goals>
+                  <goal>run</goal>
+                </goals>
+                <configuration>
+                  <target>
+                    <jar 
destfile="${project.build.directory}/${project.build.finalName}.jar"
+                         update="true">
+                      <zipfileset dir="${fory.json.mr.classes}/17"
+                                  prefix="META-INF/versions/17"
+                                  includes="**/*.class"/>
+                    </jar>
+                  </target>
+                </configuration>
+              </execution>
+              <execution>
+                <id>patch-multi-release-source-jar</id>
+                <phase>package</phase>
+                <goals>
+                  <goal>run</goal>
+                </goals>
+                <configuration>
+                  <skip>${maven.source.skip}</skip>
+                  <target>
+                    <property
+                        name="multi.release.sources.jar"
+                        
value="${project.build.directory}/${project.build.finalName}-sources.jar"/>
+                    <available
+                        file="${multi.release.sources.jar}"
+                        property="multi.release.sources.jar.present"/>
+                    <fail
+                        unless="multi.release.sources.jar.present"
+                        message="Multi-release source patching requires 
${multi.release.sources.jar}."/>
+                    <jar destfile="${multi.release.sources.jar}" update="true">
+                      <zipfileset dir="${project.basedir}/src/main/java9"
+                                  prefix="META-INF/versions/9">
+                        <include name="**/*.java"/>
+                      </zipfileset>
+                      <zipfileset dir="${project.basedir}/src/main/java17"
+                                  prefix="META-INF/versions/17">
+                        <include name="**/*.java"/>
+                      </zipfileset>
+                      <zipfileset dir="${project.basedir}/src/main/java25"
+                                  prefix="META-INF/versions/25">
+                        <include name="**/*.java"/>
+                      </zipfileset>
+                    </jar>
+                  </target>
+                </configuration>
+              </execution>
+              <execution>
+                <id>verify-graalvm-feature-jar</id>
+                <phase>verify</phase>
+                <goals>
+                  <goal>run</goal>
+                </goals>
+                <configuration>
+                  <skip>${maven.source.skip}</skip>
+                  <target>
+                    <java
+                        
classname="org.apache.fory.json.ForyJsonGraalVMFeatureJarVerifier"
+                        fork="true"
+                        failonerror="true">
+                      <classpath>
+                        <pathelement 
location="${project.build.testOutputDirectory}"/>
+                        <path refid="maven.test.classpath"/>
+                      </classpath>
+                      <arg 
value="${project.build.directory}/${project.build.finalName}.jar"/>
+                      <arg 
value="${project.build.directory}/${project.build.finalName}-sources.jar"/>
+                    </java>
+                  </target>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <profile>
+      <id>jdk25-multi-release</id>
+      <activation>
+        <jdk>[25,)</jdk>
+      </activation>
+      <build>
+        <plugins>
           <plugin>
             <groupId>org.apache.maven.plugins</groupId>
             <artifactId>maven-antrun-plugin</artifactId>
@@ -294,30 +430,6 @@
                   </target>
                 </configuration>
               </execution>
-              <execution>
-                <id>patch-jdk25-source-jar</id>
-                <phase>package</phase>
-                <goals>
-                  <goal>run</goal>
-                </goals>
-                <configuration>
-                  <target>
-                    <property
-                        name="jdk25.sources.jar"
-                        
value="${project.build.directory}/${project.build.finalName}-sources.jar"/>
-                    <available file="${jdk25.sources.jar}" 
property="jdk25.sources.jar.present"/>
-                    <fail
-                        unless="jdk25.sources.jar.present"
-                        message="Multi-release source patching requires 
${jdk25.sources.jar}."/>
-                    <jar destfile="${jdk25.sources.jar}" update="true">
-                      <zipfileset dir="${project.basedir}/src/main/java25"
-                                  prefix="META-INF/versions/25">
-                        <include name="**/*.java"/>
-                      </zipfileset>
-                    </jar>
-                  </target>
-                </configuration>
-              </execution>
             </executions>
           </plugin>
           <plugin>
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 6661c4686..64b3e446a 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
@@ -22,6 +22,7 @@ package org.apache.fory.json;
 import java.util.Objects;
 import org.apache.fory.json.codec.JsonValueCodec;
 import org.apache.fory.json.resolver.CodecRegistry;
+import org.apache.fory.platform.GraalvmSupport;
 
 /**
  * Configures and creates an immutable, thread-safe {@link ForyJson} facade.
@@ -66,13 +67,19 @@ public final class ForyJsonBuilder {
     return this;
   }
 
-  /** Enables generated object codecs for supported classes. Enabled by 
default. */
+  /**
+   * Enables generated object codecs for supported classes. Enabled by default 
and automatically
+   * disabled in a GraalVM native image.
+   */
   public ForyJsonBuilder withCodegen(boolean codegenEnabled) {
     this.codegenEnabled = codegenEnabled;
     return this;
   }
 
-  /** Enables asynchronous runtime compilation for generated object codecs. 
Enabled by default. */
+  /**
+   * Enables asynchronous runtime compilation for generated object codecs. 
Enabled by default and
+   * automatically disabled when code generation is unavailable.
+   */
   public ForyJsonBuilder withAsyncCompilation(boolean asyncCompilationEnabled) 
{
     this.asyncCompilationEnabled = asyncCompilationEnabled;
     return this;
@@ -180,11 +187,13 @@ public final class ForyJsonBuilder {
         fixedClassLoader = ForyJson.class.getClassLoader();
       }
     }
+    boolean effectiveCodegen = codegenEnabled && 
!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE;
+    boolean effectiveAsyncCompilation = asyncCompilationEnabled && 
effectiveCodegen;
     return new ForyJson(
         new JsonConfig(
             writeNullFields,
-            codegenEnabled,
-            asyncCompilationEnabled,
+            effectiveCodegen,
+            effectiveAsyncCompilation,
             propertyDiscoveryEnabled,
             propertyNamingStrategy,
             fixedClassLoader,
diff --git a/integration_tests/graalvm_tests/src/main/java/module-info.java 
b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java
similarity index 52%
copy from integration_tests/graalvm_tests/src/main/java/module-info.java
copy to 
java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java
index d3e59bb81..dc9f40867 100644
--- a/integration_tests/graalvm_tests/src/main/java/module-info.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java
@@ -17,14 +17,22 @@
  * under the License.
  */
 
-module org.apache.fory.graalvm.tests {
-  requires org.apache.fory.core;
+package org.apache.fory.json.annotation;
 
-  // Fory-generated codecs are defined by the existing codegen class loader, 
so they access the
-  // registered test models from its unnamed module.
-  exports org.apache.fory.graalvm;
-  exports org.apache.fory.graalvm.record;
+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;
 
-  opens org.apache.fory.graalvm.record to
-      org.apache.fory.core;
-}
+/**
+ * Marks a reachable JSON model for reflection metadata registration in a 
GraalVM native image.
+ *
+ * <p>The annotation does not change JSON behavior on the JVM and is 
intentionally not inherited.
+ * Annotate every runtime model type that Fory JSON reads or writes. A 
class-literal subtype listed
+ * by an annotated {@link JsonSubTypes} base is registered automatically.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface JsonType {}
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 e0019e15a..60e8cd1d6 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
@@ -661,6 +661,37 @@ final class ObjectCodecBuilder {
     return anySetter;
   }
 
+  // Native Image hosted discovery must stay aligned with this builder without 
adding duplicate
+  // property-name parsing or allocation to ordinary JVM metadata construction.
+  static boolean usesJsonMetadata(Method method, boolean record) {
+    // javac copies runtime annotations to generic bridge methods. Those 
generated methods do not
+    // own JSON declarations and processing them would reject an otherwise 
valid concrete method.
+    if (method.isSynthetic() || method.isBridge()) {
+      return false;
+    }
+    if (method.getDeclaringClass().isInterface()
+        && method.isAnnotationPresent(JsonProperty.class)) {
+      return true;
+    }
+    if (method.isAnnotationPresent(JsonAnyGetter.class)
+        || method.isAnnotationPresent(JsonAnySetter.class)) {
+      return true;
+    }
+    return !record
+        && isEligibleAccessor(method)
+        && (usesJsonReturn(method) || usesJsonParameters(method));
+  }
+
+  static boolean usesJsonReturn(Method method) {
+    // Java rejects type-use annotations on void, and setter returns are not 
JSON value owners.
+    // Keep return and parameter roles separate so hosted metadata follows the 
same ownership.
+    return method.isAnnotationPresent(JsonAnyGetter.class) || 
getterPropertyName(method) != null;
+  }
+
+  static boolean usesJsonParameters(Method method) {
+    return method.isAnnotationPresent(JsonAnySetter.class) || 
setterPropertyName(method) != null;
+  }
+
   private static FieldBuilder findAnyBuilder(
       Class<?> type, LinkedHashMap<String, FieldBuilder> builders) {
     FieldBuilder anyBuilder = null;
diff --git 
a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonTypeUse.java 
b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonTypeUse.java
index 886350562..2e179a5b6 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonTypeUse.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonTypeUse.java
@@ -39,7 +39,6 @@ import org.apache.fory.json.ForyJsonException;
 import org.apache.fory.json.annotation.JsonCodec;
 import org.apache.fory.json.codec.JsonValueCodec;
 import org.apache.fory.platform.AndroidSupport;
-import org.apache.fory.platform.GraalvmSupport;
 import org.apache.fory.reflect.TypeRef;
 import org.apache.fory.reflect.TypeUseMetadata;
 import org.apache.fory.type.TypeUtils;
@@ -56,8 +55,7 @@ import org.apache.fory.util.record.RecordComponent;
 public final class JsonTypeUse {
   private static final JsonTypeUse[] NO_CHILDREN = new JsonTypeUse[0];
   private static final String[] NO_SOURCES = new String[0];
-  private static final boolean ANNOTATIONS_SUPPORTED =
-      !AndroidSupport.IS_ANDROID && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE;
+  private static final boolean ANNOTATIONS_SUPPORTED = 
!AndroidSupport.IS_ANDROID;
 
   private final Type type;
   private final Class<?> rawType;
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 5256c236b..bd47fa5c4 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
@@ -129,8 +129,7 @@ import org.apache.fory.type.TypeUtils;
  */
 public final class JsonSharedRegistry {
   private static final int TYPE_CHECK_CACHE_LIMIT = 8192;
-  private static final boolean CODEC_ANNOTATIONS_ENABLED =
-      !AndroidSupport.IS_ANDROID && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE;
+  private static final boolean CODEC_ANNOTATIONS_ENABLED = 
!AndroidSupport.IS_ANDROID;
   private static final Comparator<DeclarationCandidate> DECLARATION_ORDER =
       new Comparator<DeclarationCandidate>() {
         @Override
@@ -655,10 +654,9 @@ public final class JsonSharedRegistry {
         hasStringEntry = true;
       }
     }
-    if (hasStringEntry && GraalvmSupport.isGraalRuntime()) {
+    if (hasStringEntry && GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
       throw new ForyJsonException(
-          "GraalVM native image requires build-time Fory codegen for 
@JsonSubTypes className entries on "
-              + baseType.getName());
+          "GraalVM native image requires @JsonSubTypes class literals on " + 
baseType.getName());
     }
     for (String className : classNames) {
       if (className != null) {
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/codec/ForyJsonGraalVMFeature.java
new file mode 100644
index 000000000..ab169a761
--- /dev/null
+++ 
b/java/fory-json/src/main/java17/org/apache/fory/json/codec/ForyJsonGraalVMFeature.java
@@ -0,0 +1,384 @@
+/*
+ * 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 java.lang.reflect.AnnotatedArrayType;
+import java.lang.reflect.AnnotatedParameterizedType;
+import java.lang.reflect.AnnotatedType;
+import java.lang.reflect.AnnotatedTypeVariable;
+import java.lang.reflect.AnnotatedWildcardType;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.GenericArrayType;
+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.Collection;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+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.JsonCodec;
+import org.apache.fory.json.annotation.JsonCreator;
+import org.apache.fory.json.annotation.JsonSubTypes;
+import org.apache.fory.json.annotation.JsonType;
+import org.apache.fory.platform.GraalvmSupport;
+import org.apache.fory.reflect.TypeRef;
+import org.graalvm.nativeimage.hosted.Feature;
+import org.graalvm.nativeimage.hosted.RuntimeReflection;
+
+/** Registers reachable Fory JSON models for GraalVM native image reflection. 
*/
+final class ForyJsonGraalVMFeature implements Feature {
+  private static final String[] SQL_TYPES = {
+    "java.sql.Date", "java.sql.Time", "java.sql.Timestamp"
+  };
+
+  private final Set<Class<?>> reachableTypes = ConcurrentHashMap.newKeySet();
+  private final Set<Class<?>> processedReachableTypes = 
ConcurrentHashMap.newKeySet();
+  private final Set<Class<?>> processedDeclarations = 
ConcurrentHashMap.newKeySet();
+  private final Set<Class<?>> processedModels = ConcurrentHashMap.newKeySet();
+  private final Set<Class<?>> processedCodecs = ConcurrentHashMap.newKeySet();
+  private final Set<Class<?>> processedContainers = 
ConcurrentHashMap.newKeySet();
+
+  @Override
+  public String getDescription() {
+    return "Registers reachable Fory JSON models for GraalVM native image";
+  }
+
+  @Override
+  public void beforeAnalysis(BeforeAnalysisAccess access) {
+    access.registerSubtypeReachabilityHandler(this::processReachableType, 
Object.class);
+  }
+
+  private void processReachableType(DuringAnalysisAccess ignored, Class<?> 
type) {
+    reachableTypes.add(type);
+  }
+
+  @Override
+  public void duringAnalysis(DuringAnalysisAccess access) {
+    if (!reachableTypes.contains(ForyJson.class)) {
+      return;
+    }
+    boolean changed = false;
+    for (Class<?> type : reachableTypes) {
+      if (processedReachableTypes.add(type)) {
+        changed |= registerContainer(type);
+        changed |= registerDeclarations(type);
+        if (type.getDeclaredAnnotation(JsonType.class) != null) {
+          changed |= registerModel(access, type);
+        }
+        if (type == ForyJson.class) {
+          registerBuiltInTypes(access);
+          changed = true;
+        }
+      }
+    }
+    if (changed) {
+      access.requireAnalysisIteration();
+    }
+  }
+
+  private boolean registerModel(DuringAnalysisAccess access, Class<?> type) {
+    if (!processedModels.add(type)) {
+      return false;
+    }
+    GraalvmSupport.registerClass(type);
+    registerContainer(type);
+    registerDeclarations(type);
+    if (!type.isEnum()
+        && !Collection.class.isAssignableFrom(type)
+        && !Map.class.isAssignableFrom(type)) {
+      registerModelHierarchy(access, type);
+    }
+    registerSubtypes(access, type);
+    return true;
+  }
+
+  private void registerModelHierarchy(BeforeAnalysisAccess access, Class<?> 
type) {
+    TypeRef<?> ownerType = TypeRef.of(type);
+    boolean record = type.isRecord();
+    for (Class<?> current = type;
+        current != null && current != Object.class;
+        current = current.getSuperclass()) {
+      for (Field field : current.getDeclaredFields()) {
+        if (isJsonField(field)) {
+          if (!current.isRecord() && Runtime.version().feature() <= 24) {
+            access.registerAsUnsafeAccessed(field);
+          }
+          registerAnnotatedType(field.getAnnotatedType());
+          
registerResolvedType(ownerType.resolveType(field.getGenericType()).getType());
+        }
+      }
+    }
+    for (Method method : type.getMethods()) {
+      if (ObjectCodecBuilder.usesJsonMetadata(method, record)) {
+        if (method.getDeclaringClass().isInterface()) {
+          RuntimeReflection.register(method);
+        }
+        if (ObjectCodecBuilder.usesJsonReturn(method)) {
+          registerAnnotatedType(method.getAnnotatedReturnType());
+          
registerResolvedType(ownerType.resolveType(method.getGenericReturnType()).getType());
+        }
+        if (ObjectCodecBuilder.usesJsonParameters(method)) {
+          registerParameterTypes(method.getParameters());
+          registerResolvedParameterTypes(ownerType, method.getParameters());
+        }
+      }
+    }
+    for (Constructor<?> constructor : type.getDeclaredConstructors()) {
+      if (constructor.isAnnotationPresent(JsonCreator.class)) {
+        registerParameterTypes(constructor.getParameters());
+        registerResolvedParameterTypes(ownerType, constructor.getParameters());
+      }
+    }
+    for (Method method : type.getDeclaredMethods()) {
+      if (method.isAnnotationPresent(JsonCreator.class)) {
+        registerParameterTypes(method.getParameters());
+        registerResolvedParameterTypes(ownerType, method.getParameters());
+      }
+    }
+    if (record) {
+      for (RecordComponent component : type.getRecordComponents()) {
+        registerAnnotatedType(component.getAnnotatedType());
+        
registerResolvedType(ownerType.resolveType(component.getGenericType()).getType());
+      }
+    }
+  }
+
+  private boolean registerDeclarations(Class<?> type) {
+    if (type == null || type == Object.class || 
!processedDeclarations.add(type)) {
+      return false;
+    }
+    boolean changed = false;
+    JsonCodec annotation = type.getDeclaredAnnotation(JsonCodec.class);
+    if (annotation != null) {
+      RuntimeReflection.register(type);
+      registerCodec(annotation.value());
+      changed = true;
+    }
+    changed |= registerDeclarations(type.getSuperclass());
+    for (Class<?> interfaceType : type.getInterfaces()) {
+      changed |= registerDeclarations(interfaceType);
+    }
+    return changed;
+  }
+
+  private void registerParameterTypes(Parameter[] parameters) {
+    for (Parameter parameter : parameters) {
+      registerAnnotatedType(parameter.getAnnotatedType());
+    }
+  }
+
+  private void registerResolvedParameterTypes(TypeRef<?> ownerType, 
Parameter[] parameters) {
+    for (Parameter parameter : parameters) {
+      
registerResolvedType(ownerType.resolveType(parameter.getParameterizedType()).getType());
+    }
+  }
+
+  private void registerResolvedType(Type type) {
+    Set<TypeVariable<?>> visiting = Collections.newSetFromMap(new 
IdentityHashMap<>());
+    registerResolvedType(type, visiting);
+  }
+
+  private void registerResolvedType(Type type, Set<TypeVariable<?>> visiting) {
+    if (type == null) {
+      return;
+    }
+    registerContainer(type);
+    if (type instanceof ParameterizedType) {
+      ParameterizedType parameterizedType = (ParameterizedType) type;
+      registerResolvedType(parameterizedType.getOwnerType(), visiting);
+      for (Type argument : parameterizedType.getActualTypeArguments()) {
+        registerResolvedType(argument, visiting);
+      }
+    } else if (type instanceof GenericArrayType) {
+      registerResolvedType(((GenericArrayType) 
type).getGenericComponentType(), visiting);
+    } else if (type instanceof WildcardType) {
+      WildcardType wildcardType = (WildcardType) type;
+      registerResolvedTypes(wildcardType.getUpperBounds(), visiting);
+      registerResolvedTypes(wildcardType.getLowerBounds(), visiting);
+    } else if (type instanceof TypeVariable<?>) {
+      TypeVariable<?> variable = (TypeVariable<?>) type;
+      if (visiting.add(variable)) {
+        registerResolvedTypes(variable.getBounds(), visiting);
+        visiting.remove(variable);
+      }
+    }
+  }
+
+  private void registerResolvedTypes(Type[] types, Set<TypeVariable<?>> 
visiting) {
+    for (Type type : types) {
+      registerResolvedType(type, visiting);
+    }
+  }
+
+  private void registerAnnotatedType(AnnotatedType type) {
+    Set<TypeVariable<?>> visiting = Collections.newSetFromMap(new 
IdentityHashMap<>());
+    registerAnnotatedType(type, visiting);
+  }
+
+  private void registerAnnotatedType(AnnotatedType type, Set<TypeVariable<?>> 
visiting) {
+    if (type == null) {
+      return;
+    }
+    registerContainer(type.getType());
+    JsonCodec annotation = type.getDeclaredAnnotation(JsonCodec.class);
+    if (annotation != null) {
+      registerCodec(annotation.value());
+    }
+    if (type instanceof AnnotatedParameterizedType) {
+      AnnotatedParameterizedType parameterizedType = 
(AnnotatedParameterizedType) type;
+      registerAnnotatedType(parameterizedType.getAnnotatedOwnerType(), 
visiting);
+      for (AnnotatedType argument : 
parameterizedType.getAnnotatedActualTypeArguments()) {
+        registerAnnotatedType(argument, visiting);
+      }
+    } else if (type instanceof AnnotatedArrayType) {
+      registerAnnotatedType(
+          ((AnnotatedArrayType) type).getAnnotatedGenericComponentType(), 
visiting);
+    } else if (type instanceof AnnotatedWildcardType) {
+      AnnotatedWildcardType wildcardType = (AnnotatedWildcardType) type;
+      registerAnnotatedTypes(wildcardType.getAnnotatedUpperBounds(), visiting);
+      registerAnnotatedTypes(wildcardType.getAnnotatedLowerBounds(), visiting);
+    } else if (type instanceof AnnotatedTypeVariable) {
+      TypeVariable<?> variable = (TypeVariable<?>) type.getType();
+      if (visiting.add(variable)) {
+        registerAnnotatedTypes(((AnnotatedTypeVariable) 
type).getAnnotatedBounds(), visiting);
+        visiting.remove(variable);
+      }
+    }
+  }
+
+  private void registerAnnotatedTypes(AnnotatedType[] types, 
Set<TypeVariable<?>> visiting) {
+    for (AnnotatedType type : types) {
+      registerAnnotatedType(type, visiting);
+    }
+  }
+
+  private void registerCodec(Class<? extends JsonValueCodec<?>> codecClass) {
+    if (!processedCodecs.add(codecClass)) {
+      return;
+    }
+    RuntimeReflection.register(codecClass);
+    try {
+      RuntimeReflection.register(codecClass.getConstructor());
+    } catch (NoSuchMethodException e) {
+      throw new IllegalStateException(
+          "@JsonCodec class must have a public no-argument constructor: " + 
codecClass.getName(),
+          e);
+    }
+  }
+
+  private boolean registerContainer(Type type) {
+    Class<?> rawType = null;
+    if (type instanceof Class<?>) {
+      rawType = (Class<?>) type;
+    } else if (type instanceof ParameterizedType) {
+      Type parameterizedRawType = ((ParameterizedType) type).getRawType();
+      if (parameterizedRawType instanceof Class<?>) {
+        rawType = (Class<?>) parameterizedRawType;
+      }
+    }
+    if (rawType == null
+        || rawType.isInterface()
+        || Modifier.isAbstract(rawType.getModifiers())
+        || (!Collection.class.isAssignableFrom(rawType) && 
!Map.class.isAssignableFrom(rawType))
+        || !processedContainers.add(rawType)) {
+      return false;
+    }
+    try {
+      RuntimeReflection.register(rawType.getConstructor());
+      return true;
+    } catch (NoSuchMethodException ignored) {
+      // CollectionCodec and MapCodec preserve the same runtime failure for a 
concrete container
+      // without a public no-argument constructor.
+      return false;
+    }
+  }
+
+  private void registerSubtypes(DuringAnalysisAccess access, Class<?> type) {
+    JsonSubTypes annotation = type.getDeclaredAnnotation(JsonSubTypes.class);
+    if (annotation == null) {
+      return;
+    }
+    for (JsonSubTypes.Type entry : annotation.value()) {
+      Class<?> subtype = entry.value();
+      if (subtype != Void.class) {
+        registerModel(access, subtype);
+      }
+    }
+  }
+
+  private void registerSqlTypes(DuringAnalysisAccess access) {
+    for (String className : SQL_TYPES) {
+      Class<?> type = access.findClassByName(className);
+      if (type != null) {
+        RuntimeReflection.register(type);
+        try {
+          RuntimeReflection.register(type.getConstructor(long.class));
+        } catch (NoSuchMethodException e) {
+          throw new IllegalStateException("Missing Fory JSON SQL constructor 
for " + className, e);
+        }
+      }
+    }
+  }
+
+  private void registerBuiltInTypes(DuringAnalysisAccess access) {
+    registerSqlTypes(access);
+    registerBigDecimalFields(access);
+  }
+
+  private void registerBigDecimalFields(BeforeAnalysisAccess access) {
+    registerBigDecimalField(access, "intCompact", long.class);
+    registerBigDecimalField(access, "intVal", BigInteger.class);
+    registerBigDecimalField(access, "scale", int.class);
+  }
+
+  private void registerBigDecimalField(
+      BeforeAnalysisAccess access, String fieldName, Class<?> fieldType) {
+    try {
+      Field field = BigDecimal.class.getDeclaredField(fieldName);
+      if (field.getType() == fieldType) {
+        RuntimeReflection.register(field);
+        if (Runtime.version().feature() <= 24) {
+          access.registerAsUnsafeAccessed(field);
+        }
+      }
+    } catch (NoSuchFieldException ignored) {
+      // BigDecimalFields preserves its public-JDK fallback if a future JDK 
changes this layout.
+    }
+  }
+
+  private static boolean isJsonField(Field field) {
+    int modifiers = field.getModifiers();
+    return !Modifier.isStatic(modifiers)
+        && !Modifier.isTransient(modifiers)
+        && field.getType() != Class.class
+        && !field.isSynthetic();
+  }
+}
diff --git a/java/fory-json/src/main/java9/module-info.java 
b/java/fory-json/src/main/java9/module-info.java
index b93aeb3f9..f5a779a25 100644
--- a/java/fory-json/src/main/java9/module-info.java
+++ b/java/fory-json/src/main/java9/module-info.java
@@ -21,6 +21,8 @@ module org.apache.fory.json {
   requires org.apache.fory.core;
   requires static java.sql;
   requires static com.google.common;
+  requires static org.graalvm.nativeimage;
+  requires static org.graalvm.sdk;
 
   exports org.apache.fory.json;
   exports org.apache.fory.json.annotation;
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
new file mode 100644
index 000000000..b3943fab7
--- /dev/null
+++ 
b/java/fory-json/src/main/resources/META-INF/native-image/org.apache.fory/fory-json/native-image.properties
@@ -0,0 +1,18 @@
+# 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.
+
+Args=--features=org.apache.fory.json.codec.ForyJsonGraalVMFeature
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
 
b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java
similarity index 82%
copy from 
java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
copy to 
java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java
index 5683ad4c0..dc74e0e05 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
+++ 
b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package org.apache.fory.builder;
+package org.apache.fory.json;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -35,30 +35,30 @@ import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
 
-/** Verifies the packaged GraalVM Feature and its Native Image activation 
metadata. */
-public final class GraalvmFeatureJarVerifier {
-  private static final String FEATURE_CLASS_NAME = 
"org.apache.fory.platform.ForyGraalVMFeature";
+/** 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_FILE =
-      "org/apache/fory/platform/ForyGraalVMFeature.class";
+      "org/apache/fory/json/codec/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/platform/ForyGraalVMFeature.java";
+      "org/apache/fory/json/codec/ForyJsonGraalVMFeature.java";
   private static final String VERSION_17_FEATURE_SOURCE =
       "META-INF/versions/17/" + FEATURE_SOURCE_FILE;
   private static final String NATIVE_IMAGE_PROPERTIES =
-      
"META-INF/native-image/org.apache.fory/fory-core/native-image.properties";
+      
"META-INF/native-image/org.apache.fory/fory-json/native-image.properties";
   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 INITIALIZATION_OPTION = 
"--initialize-at-build-time=";
 
-  private GraalvmFeatureJarVerifier() {}
+  private ForyJsonGraalVMFeatureJarVerifier() {}
 
   public static void main(String[] args) throws Exception {
     if (args.length != 2) {
       throw new IllegalArgumentException(
-          "Usage: GraalvmFeatureJarVerifier <fory-core.jar> 
<fory-core-sources.jar>");
+          "Usage: ForyJsonGraalVMFeatureJarVerifier <fory-json.jar> 
<sources.jar>");
     }
     Path jarPath = Paths.get(args[0]);
     verifyBinaryJar(jarPath);
@@ -82,16 +82,10 @@ public final class GraalvmFeatureJarVerifier {
       check(countEntries(jarFile, FEATURE_SERVICE) == 0, "Feature service file 
must not exist");
       check(
           countEntries(jarFile, NATIVE_IMAGE_PROPERTIES) == 1,
-          "Expected exactly one core native-image.properties");
-
+          "Expected exactly one JSON native-image.properties");
       String properties = readEntry(jarFile, NATIVE_IMAGE_PROPERTIES);
-      check(
-          countOccurrences(properties, "--features=") == 1,
-          "Expected exactly one --features option");
-      check(properties.contains(FEATURE_OPTION), "Core Feature option is 
missing");
-      check(
-          properties.contains(INITIALIZATION_OPTION),
-          "Build-time initialization option is missing");
+      check(countOccurrences(properties, "--features=") == 1, "Expected one 
--features option");
+      check(properties.contains(FEATURE_OPTION), "Fory JSON Feature option is 
missing");
     }
   }
 
@@ -110,10 +104,9 @@ public final class GraalvmFeatureJarVerifier {
   private static void verifyFeatureLoading(Path jarPath) throws Exception {
     URL[] urls = {jarPath.toUri().toURL()};
     try (URLClassLoader classLoader =
-        new URLClassLoader(urls, 
GraalvmFeatureJarVerifier.class.getClassLoader())) {
+        new URLClassLoader(urls, 
ForyJsonGraalVMFeatureJarVerifier.class.getClassLoader())) {
       Class<?> featureClass = Class.forName(FEATURE_CLASS_NAME, true, 
classLoader);
-      check(
-          featureClass.getClassLoader() == classLoader, "Feature was not 
loaded from packaged jar");
+      check(featureClass.getClassLoader() == classLoader, "Feature was not 
loaded from the jar");
       check(!Modifier.isPublic(featureClass.getModifiers()), "Feature must 
remain non-public");
       Constructor<?> constructor = featureClass.getDeclaredConstructor();
       constructor.setAccessible(true);
@@ -122,7 +115,7 @@ public final class GraalvmFeatureJarVerifier {
       getDescription.setAccessible(true);
       Object description = getDescription.invoke(feature);
       check(
-          description instanceof String && ((String) 
description).contains("Fory"),
+          description instanceof String && ((String) 
description).contains("Fory JSON"),
           "Packaged Feature returned an invalid description");
     }
   }


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

Reply via email to