GraalVM Support
GraalVM Native Image
GraalVM Native Image compiles Java applications ahead of time. Because a native image cannot discover every reflective access or generate serializers at runtime, Fory prepares serializers and the required metadata while the image is built.
fory-core contains Fory's GraalVM Feature and activates it automatically. Applications do not need
an additional Fory artifact or a --features option.
How It Works
Prepare each Fory instance during build-time class initialization:
- Store the Fory instance in a static field.
- Register every application class that the native executable will serialize.
- Call
fory.ensureSerializersCompiled()after registration is complete. - Configure the owning class for build-time initialization.
The Feature uses those registrations to provide the Native Image metadata required by Fory, including metadata for private constructors, records, serializer constructors, and registered proxy shapes. Application classes still need to be registered with Fory before serializers are compiled.
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. Add the Fory annotation processor to the application compiler path:
<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:
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 processor also supports Fory JSON Mixins for models that cannot be modified:
import org.apache.fory.json.ForyJson;
import org.apache.fory.json.annotation.JsonMixin;
import org.apache.fory.json.annotation.JsonProperty;
@JsonMixin(target = ThirdPartyUser.class)
public abstract class ThirdPartyUserMixin {
@JsonProperty("user_id")
long id;
}
public class JsonExample {
public static void main(String[] args) {
ForyJson json =
ForyJson.builder().registerMixin(ThirdPartyUserMixin.class).build();
ThirdPartyUser user = json.fromJson("{\"user_id\":1}", ThirdPartyUser.class);
System.out.println(json.toJson(user));
}
}
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.
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.
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.
JsonValue fields and effective public zero-argument methods are supported, including matching
one-String JsonCreator constructors and public static factories. Fixed JsonRawValue fields and
getters support trusted raw String values, and fixed JsonBase64 fields and getters support Base64
byte[] values as on the JVM. For direct target annotations, annotate each reachable owning model
with JsonType so Native Image retains these members and the Base64 codec constructor. A directly
annotated JsonValue Record uses its generated component accessor and canonical constructor
operations. An effective declaration supplied by a Mixin uses the Mixin workflow above instead.
JsonAnyProperty and JsonAnyGetter flatten their Map into the enclosing object. Use
@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.
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.
Basic Usage
Create Fory and Register Classes
import org.apache.fory.Fory;
public class Example {
private static final Fory FORY;
static {
FORY = Fory.builder().withXlang(false).build();
FORY.register(MyClass.class);
FORY.register(AnotherClass.class);
FORY.ensureSerializersCompiled();
}
public static void main(String[] args) {
byte[] bytes = FORY.serialize(new MyClass());
MyClass obj = (MyClass) FORY.deserialize(bytes);
}
}
Configure Build-Time Initialization
Create resources/META-INF/native-image/your-group/your-artifact/native-image.properties:
Args = --initialize-at-build-time=com.example.Example
Registered Classes
During the native-image build, Fory automatically registers the metadata needed for registered classes, including:
- Classes with private constructors
- Private nested classes and records
- Serializer constructors
- Dynamic proxy shapes registered through
GraalvmSupport
For Fory, your application metadata only needs to configure its build-time initialized bootstrap class, for example:
Args = --initialize-at-build-time=com.example.Example
Example with Private Record
import org.apache.fory.Fory;
public class Example {
private record PrivateRecord(int id, String name) {}
private static final Fory FORY;
static {
FORY = Fory.builder().withXlang(false).build();
FORY.register(PrivateRecord.class);
FORY.ensureSerializersCompiled();
}
}
Example with Dynamic Proxy
import org.apache.fory.Fory;
import org.apache.fory.platform.GraalvmSupport;
public class ProxyExample {
public interface MyService {
String execute();
}
public interface Audited {
String traceId();
}
private static final Fory FORY;
static {
FORY = Fory.builder().withXlang(false).build();
GraalvmSupport.registerProxySupport(MyService.class, Audited.class);
FORY.ensureSerializersCompiled();
}
}
Use registerProxySupport(MyService.class) for a single-interface proxy. For proxies that implement
multiple interfaces, pass the full interface list in the same order used to create the proxy. Call
this method before ensureSerializersCompiled().
Thread-Safe Fory
For multi-threaded applications, use ThreadLocalFory:
import java.util.List;
import org.apache.fory.Fory;
import org.apache.fory.ThreadLocalFory;
import org.apache.fory.ThreadSafeFory;
public class ThreadSafeExample {
public record Foo(int f1, String f2, List<String> f3) {}
private static final ThreadSafeFory FORY;
static {
FORY =
new ThreadLocalFory(
builder -> {
Fory f = builder.build();
f.register(Foo.class);
f.ensureSerializersCompiled();
return f;
});
}
public static void main(String[] args) {
Foo foo = new Foo(10, "abc", List.of("str1", "str2"));
byte[] bytes = FORY.serialize(foo);
Foo result = (Foo) FORY.deserialize(bytes);
}
}
Troubleshooting
"Type is instantiated reflectively but was never registered"
If you see this error:
Type com.example.MyClass is instantiated reflectively but was never registered
Register the class before compiling serializers:
fory.register(MyClass.class);
fory.ensureSerializersCompiled();
If registration is conditional, make sure the same branch runs during build-time initialization.
Framework Integration
For framework developers integrating Fory:
- Provide a configuration file for users to list serializable classes.
- Load those classes and call
fory.register(Class<?>)for each. - Call
fory.ensureSerializersCompiled()after all registrations. - Configure your integration class for build-time initialization.
Benchmark
Performance comparison between Fory and GraalVM JDK Serialization:
| Type | Compression | Speed | Size |
|---|---|---|---|
| Struct | Off | 46x faster | 43% |
| Struct | On | 24x faster | 31% |
| Pojo | Off | 12x faster | 56% |
| Pojo | On | 12x faster | 48% |
See Benchmark.java for benchmark code.
Struct Benchmark
Class Fields
public class Struct implements Serializable {
public int f1;
public long f2;
public float f3;
public double f4;
public int f5;
public long f6;
public float f7;
public double f8;
public int f9;
public long f10;
public float f11;
public double f12;
}
Benchmark Results
No compression:
Benchmark repeat number: 400000
Object type: class org.apache.fory.graalvm.Struct
Compress number: false
Fory size: 76.0
JDK size: 178.0
Fory serialization took mills: 49
JDK serialization took mills: 2254
Compare speed: Fory is 45.70x speed of JDK
Compare size: Fory is 0.43x size of JDK
Compress number:
Benchmark repeat number: 400000
Object type: class org.apache.fory.graalvm.Struct
Compress number: true
Fory size: 55.0
JDK size: 178.0
Fory serialization took mills: 130
JDK serialization took mills: 3161
Compare speed: Fory is 24.16x speed of JDK
Compare size: Fory is 0.31x size of JDK
Pojo Benchmark
Class Fields
public class Foo implements Serializable {
int f1;
String f2;
List<String> f3;
Map<String, Long> f4;
}
Benchmark Results
No compression:
Benchmark repeat number: 400000
Object type: class org.apache.fory.graalvm.Foo
Compress number: false
Fory size: 541.0
JDK size: 964.0
Fory serialization took mills: 1663
JDK serialization took mills: 16266
Compare speed: Fory is 12.19x speed of JDK
Compare size: Fory is 0.56x size of JDK
Compress number:
Benchmark repeat number: 400000
Object type: class org.apache.fory.graalvm.Foo
Compress number: true
Fory size: 459.0
JDK size: 964.0
Fory serialization took mills: 1289
JDK serialization took mills: 15069
Compare speed: Fory is 12.11x speed of JDK
Compare size: Fory is 0.48x size of JDK