Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ jobs:
- name: Install Fory Java artifacts
run: |
cd java
mvn -T16 --no-transfer-progress -pl fory-core,fory-annotation-processor -am install -DskipTests -Dmaven.javadoc.skip=true -Dmaven.source.skip=true
mvn -T16 --no-transfer-progress -pl fory-json,fory-annotation-processor -am install -DskipTests -Dmaven.javadoc.skip=true -Dmaven.source.skip=true
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
Expand All @@ -329,7 +329,8 @@ jobs:
working-directory: integration_tests/android_tests
script: |
yes | sdkmanager "platforms;android-${{ matrix.api-level }}" "build-tools;35.0.0"
gradle --no-daemon --stacktrace connectedCheck
if [ "${{ matrix.api-level }}" = "26" ]; then gradle --no-daemon --stacktrace -PforyTestBuildType=debug connectedCheck; fi
gradle --no-daemon --stacktrace -PforyTestBuildType=release connectedCheck
- name: Upload Android Test Report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
Expand Down
96 changes: 96 additions & 0 deletions docs/guide/java/android-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,101 @@ Android serialization on the non-codegen path and logs a warning.
Android apps that need generated serializers should use build-time static generated serializers
instead.

## Fory JSON

Fory JSON supports ordinary classes on Android API level 26 and later through the regular
`fory-json` artifact. Runtime JSON code generation and asynchronous compilation are disabled
automatically, so `ForyJson.builder().build()` uses the interpreted object mapper.

Add Fory JSON to the application and put the annotation processor on the module's processor path:

```kotlin
dependencies {
implementation("org.apache.fory:fory-json:${foryVersion}")
annotationProcessor("org.apache.fory:fory-annotation-processor:${foryVersion}")
}
```

`@JsonType` is optional. Add it to application models when the processor should emit exact R8 rules
and metadata for `@JsonCodec` type uses, including qualified roots, parameter types, generic
arguments, and array components:

```java
import java.util.List;
import org.apache.fory.json.annotation.JsonCodec;
import org.apache.fory.json.annotation.JsonType;

@JsonType
public final class GeneratedInvoice {
public List<@JsonCodec(MoneyCodec.class) Money> items;

public GeneratedInvoice() {}
}
```

Without `@JsonType`, ordinary reflection mapping and `@JsonCodec` declarations on types, fields, and
effective ordinary getters still work. A release-minified application must supply equivalent exact
R8 rules for every reflected class, constructor, field, method, generic signature, and runtime
annotation. For example:

```java
public final class ManualInvoice {
@JsonCodec(MoneyCodec.class)
public Money total;

public ManualInvoice() {}
}
```

```proguard
-keepattributes Signature,RuntimeVisibleAnnotations,RuntimeVisibleParameterAnnotations,RuntimeVisibleTypeAnnotations,AnnotationDefault,MethodParameters
-keep,allowoptimization class com.example.ManualInvoice {
public <init>();
public com.example.Money total;
}
-keep,allowoptimization,allowobfuscation class com.example.MoneyCodec {
public <init>();
}
```

Application-authored rules cannot supply codec type-use metadata that Android reflection does not
expose. For example, this nested annotation is not applied on Android when the class omits
`@JsonType`, even if exact rules retain the class, field, generic signature, and type annotation:

```java
public final class UnsupportedNestedInvoice {
public List<@JsonCodec(MoneyCodec.class) Money> items;

public UnsupportedNestedInvoice() {}
}
```

```proguard
-keepattributes Signature,RuntimeVisibleTypeAnnotations
-keep,allowoptimization class com.example.UnsupportedNestedInvoice {
public <init>();
public java.util.List items;
}
```

On Android, a pure type-use `@JsonCodec` requires `@JsonType`. This includes a qualified root type
use, a parameter type, a generic argument, and an array component. Type declarations, field
declarations, and effective ordinary getter declarations remain available through direct
reflection without `@JsonType`.

On the JVM and in a native image, a field or effective ordinary getter declaration takes precedence
over a codec on the root annotated type. Android reads the declaration directly and obtains pure
type-use facts from the generated companion. A setter, creator factory, unrelated method, or void
method cannot declare `@JsonCodec`. `JsonAnyProperty` and `JsonAnyGetter` flatten their Map rather
than exposing a complete root value, so their root declaration and root type-use codec forms are
invalid; annotate the Map value type when a nested codec is needed.

Android Fory JSON requires a retained no-argument constructor for an ordinary mutable class; it may
be non-public when Android reflection can make it accessible. `JsonCreator` constructor-backed
classes follow the normal creator rules instead. Retain every field and method used for reflection,
or use an application codec when a model cannot satisfy those requirements. Fory JSON Record
mapping is not supported on Android.

## Static Generated Serializers

Use `@ForyStruct` static generated serializers for Android application classes. They are generated by
Expand Down Expand Up @@ -115,6 +210,7 @@ The following JVM features are not supported on Android:
- Native-address serialization APIs and native-address `MemoryBuffer` wrapping.
- Raw unsafe memory copy APIs.
- `java/fory-format` row-format APIs.
- Fory JSON Record mapping.

## ByteBuffer

Expand Down
17 changes: 13 additions & 4 deletions docs/guide/java/graalvm-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,19 @@ other builder options retain their normal behavior. Applications can create diff
`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.
Type, field, effective ordinary getter, inherited, and nested type-use `@JsonCodec` annotations are
supported. For a field or getter, the declaration is checked first and the root annotated type is
used only when the declaration is absent. Nested generic arguments and array components remain
fully supported in a native image.

`JsonAnyProperty` and `JsonAnyGetter` flatten their Map and have no complete root value, so a field
or method declaration codec and a root type-use codec are invalid there. A nested codec on the Map
value remains 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

Expand Down
83 changes: 59 additions & 24 deletions docs/guide/java/json-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,10 @@ 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.
`JsonType` marks a reachable object model for GraalVM Native Image metadata. On Android it also
enables processor-generated R8 rules and `JsonCodec` type-use metadata. It has no effect on ordinary
JVM JSON behavior and is not inherited. See [GraalVM Support](graalvm-support.md) and
[Android Support](android-support.md) for the platform workflows.

### `JsonProperty`

Expand Down Expand Up @@ -738,6 +739,27 @@ The default applies at root `Class` and raw `TypeRef` targets and at unannotated
positions. Fory explicitly inherits declarations through superclasses and interfaces; it does not
use Java `@Inherited`.

On a field or effective ordinary getter, a declaration annotation selects the codec for that
member's root value:

```java
public final class Invoice {
@JsonCodec(MoneyCodec.class)
public Money total;

@JsonCodec(MoneyCodec.class)
public Money getTax() {
return tax;
}
}
```

The method form is invalid on record accessors, setters, creator factories, unrelated methods, and
void methods. An ordinary getter declaration is invalid when field mode disables getter discovery.
Existing record-component `@JsonCodec` behavior is unchanged; declaring `@JsonCodec` only on a
record accessor is unsupported. For a selected field or getter, Fory checks the declaration first
and uses the annotation on the root annotated type only when the declaration is absent.

Use the same annotation at a type-use position to select a codec for exactly that occurrence:

```java
Expand All @@ -749,7 +771,9 @@ import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

public final class Invoice {
public @JsonCodec(MoneyCodec.class) Money total;
// Qualified placement selects the root type-use fallback rather than the field declaration.
public com.example.@JsonCodec(MoneyCodec.class) Money qualifiedTotal;

public List<@JsonCodec(MoneyCodec.class) Money> items;
public Set<@JsonCodec(MoneyCodec.class) Money> uniqueItems;
public Collection<@JsonCodec(MoneyCodec.class) Money> allItems;
Expand All @@ -758,7 +782,7 @@ public final class Invoice {
public AtomicReference<@JsonCodec(MoneyCodec.class) Money> current;

// Codec for each element.
public @JsonCodec(MoneyCodec.class) Money[] itemArray;
public com.example.@JsonCodec(MoneyCodec.class) Money[] itemArray;

// Codec for the complete array value.
public Money @JsonCodec(MoneyArrayCodec.class) [] encodedArray;
Expand Down Expand Up @@ -793,17 +817,21 @@ discovered model property, declare a default on `Money`, or use exact builder re
Fory chooses the complete codec for each current resolved value node using this exact order. An
invalid higher-priority source fails rather than falling back.

| Priority | Source | Matching rule |
| -------: | --------------------------------------- | ------------------------------------------------------------------------- |
| 1 | Current `TYPE_USE @JsonCodec` | Exact resolved occurrence after property merging and generic substitution |
| 2 | `registerCodec(Target.class, instance)` | Exact target `Class`; never inherited |
| 3 | Direct `@JsonCodec` declaration | Current class, record, enum, or interface |
| 4 | Inherited `@JsonCodec` declaration | Most-specific superclass/interface result |
| 5 | Existing mapping | `JsonSubTypes`, built-in/container mapping, then default object mapping |
| Priority | Source | Matching rule |
| -------: | --------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| 1 | Current member or `TYPE_USE @JsonCodec` | Exact resolved occurrence after declaration-first acquisition, property merging, and generic substitution |
| 2 | `registerCodec(Target.class, instance)` | Exact target `Class`; never inherited |
| 3 | Direct type `@JsonCodec` declaration | Current class, record, enum, or interface |
| 4 | Inherited type declaration | Most-specific superclass/interface result |
| 5 | Existing mapping | `JsonSubTypes`, built-in/container mapping, then default object mapping |

For each field or getter, its declaration takes precedence only over that same member's root
annotated type. The selected field, getter, setter, record component, and creator occurrences are
then merged as one logical property; different codecs at the same node remain a conflict.

Rows 1 through 4 replace the whole current value mapping. They may therefore replace a scalar,
enum, container, object, or `JsonSubTypes` representation. A current type-use, exact registration,
or direct declaration also resolves any lower-priority inherited ambiguity.
enum, container, object, or `JsonSubTypes` representation. A current occurrence, exact
registration, or direct declaration also resolves any lower-priority inherited ambiguity.

### Deterministic inheritance and conflicts

Expand Down Expand Up @@ -911,11 +939,13 @@ map values, Optional, and atomic references resolve their annotated children nor

Map keys are JSON object member names, not JSON values. An explicit `@JsonCodec` anywhere in a Map
key subtree is rejected. Builder or declaration value codecs on the key's raw type are ignored in
key position and still apply when that type is used as a value. For `JsonAnyProperty`,
`JsonAnyGetter`, and `JsonAnySetter`, the outer Map is flattened and cannot select a whole-value
codec; only its value node can. Wildcards, wildcard bounds, and type variables still unresolved
after substitution cannot select a codec. A type-use on an `extends` or `implements` clause is not
a JSON value occurrence and is not used; annotate the type declaration instead. Annotation type
key position and still apply when that type is used as a value. `JsonAnyProperty` and
`JsonAnyGetter` flatten their Map and cannot select a whole-value codec; only the nested Map value
node can. A field or method declaration codec and a root type-use codec are therefore invalid on
those two forms. A `JsonAnySetter` value parameter is already one flattened value and may carry a
root type-use codec. Wildcards, wildcard bounds, and type variables still unresolved after
substitution cannot select a codec. A type-use on an `extends` or `implements` clause is not a JSON
value occurrence and is not used; annotate the type declaration instead. Annotation type
declarations are not supported JSON model targets.

### Construction, inherited results, and platform support
Expand All @@ -934,11 +964,16 @@ 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 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.
`@JsonCodec` is supported on ordinary JVMs and GraalVM native images, including member
declarations, inherited type 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 directly reads type, field, and effective getter declarations. Pure type-use locations,
including qualified roots, parameters, generic arguments, and array components, require `JsonType`
processor metadata. `JsonType` also supplies automatic R8 rules; applications that omit it can
supply exact rules manually and use declaration syntax, but type-use codecs remain unavailable. See
[Android Support](android-support.md).

## Type validation and untrusted input

Expand Down
19 changes: 13 additions & 6 deletions integration_tests/android_tests/README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
# Android Integration Tests

This project runs Android API 26+ instrumented tests for Java `fory-core`. The
instrumented tests run against the release build type so R8/minification covers
the static generated serializer path.
This project runs Android API 26+ instrumented tests for Java `fory-core` and
`fory-json`. API 26 runs both debug reflection coverage and the release-minified
suite. API 36 runs the release-minified suite. Release coverage verifies static
serializers, processor-generated Fory JSON rules and codec type-use metadata, and
equivalent application-authored exact rules.

The tests consume `org.apache.fory:fory-core:1.4.0-SNAPSHOT` and
The tests consume `org.apache.fory:fory-core:1.4.0-SNAPSHOT`,
`org.apache.fory:fory-json:1.4.0-SNAPSHOT`, and
`org.apache.fory:fory-annotation-processor:1.4.0-SNAPSHOT` from the local Maven
repository, so install the Java artifacts before running Gradle:

```bash
cd ../../java
mvn -T16 --no-transfer-progress -pl fory-core,fory-annotation-processor -am install -DskipTests -Dmaven.javadoc.skip=true -Dmaven.source.skip=true
mvn -T16 --no-transfer-progress -pl fory-json,fory-annotation-processor -am install -DskipTests -Dmaven.javadoc.skip=true -Dmaven.source.skip=true
cd ../integration_tests/android_tests
gradle --no-daemon connectedCheck
gradle --no-daemon -PforyTestBuildType=debug connectedCheck
gradle --no-daemon -PforyTestBuildType=release connectedCheck
```

The `foryTestBuildType` property is test-only. It selects the target build type
used by the instrumentation suite; production build configuration is unchanged.

`java/fory-format` is intentionally not covered here because it is not part of
the Android support surface.
5 changes: 4 additions & 1 deletion integration_tests/android_tests/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ buildscript {

apply plugin: 'com.android.application'

def foryTestBuildType = providers.gradleProperty('foryTestBuildType').getOrElse('release')

android {
namespace 'org.apache.fory.android.tests'
compileSdk 35
Expand All @@ -58,7 +60,7 @@ android {
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}

testBuildType 'release'
testBuildType foryTestBuildType

buildTypes {
release {
Expand Down Expand Up @@ -86,6 +88,7 @@ dependencies {
exclude group: 'com.google.guava', module: 'guava'
exclude group: 'org.codehaus.janino', module: 'janino'
}
implementation 'org.apache.fory:fory-json:1.4.0-SNAPSHOT'
annotationProcessor 'org.apache.fory:fory-annotation-processor:1.4.0-SNAPSHOT'
implementation 'com.google.guava:guava:32.1.2-android'
implementation 'org.slf4j:slf4j-api:2.0.12'
Expand Down
Loading
Loading