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: 5 additions & 0 deletions .agents/languages/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ Load this file when changing anything under `java/` or when Java drives a cross-
and locale types, `Float16`, `BFloat16`, and user-defined types remain registerable. Field/type
`@JsonCodec`, `@JsonFormat`, and semantic metadata remain separate from exact registry mutation
and are fixed by the target class or effective Mixin.
- Fory JSON `byte[]` defaults to a Base64 string. `@JsonByteArray` selects required
`Format.BASE64` or `Format.ARRAY` for an exact byte-array field or getter in both directions.
Keep selection in the existing property codec path, including Mixin, Java processor, Kotlin
KSP, and GraalVM handling. Numeric arrays use signed-byte semantics and graph-memory accounting;
Base64 values remain binary leaves outside that budget.
- Fory JSON `ObjectCodec` instances are resolver-owned and must not be registered directly. A
language module that supplies a custom object model must use a `JsonCodecFactory`. A configurable
factory's stable key must cover every option that can change its created codec class, object
Expand Down
6 changes: 3 additions & 3 deletions docs/json/android.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,12 @@ on the `ForyJson` builder that should use it:

```java
import org.apache.fory.json.ForyJson;
import org.apache.fory.json.annotation.JsonBase64;
import org.apache.fory.json.annotation.JsonByteArray;
import org.apache.fory.json.annotation.JsonMixin;

@JsonMixin(target = ThirdPartyInvoice.class)
public abstract class ThirdPartyInvoiceMixin {
@JsonBase64 byte[] signature;
@JsonByteArray(JsonByteArray.Format.BASE64) byte[] signature;
}

ForyJson json =
Expand Down Expand Up @@ -191,7 +191,7 @@ This reflection-based section applies to Java models. Kotlin models use the Kotl
a minified Android build, apply KSP instead of writing broad package keep rules.

Java `@JsonType` models support effective `JsonValidator`, `JsonValue`, `JsonRawValue`,
`JsonBase64`, and `JsonFormat` annotations. Without `@JsonType`, those annotations still work
`JsonByteArray`, and `JsonFormat` annotations. Without `@JsonType`, those annotations still work
through reflection, but a release-minified application must keep the exact annotated members,
annotation attributes, and codec constructor itself. A `JsonValue` method may use a non-JavaBean
name, so its manual rule must name that method explicitly.
Expand Down
36 changes: 22 additions & 14 deletions docs/json/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ license: |

Fory JSON provides these mapping and validation annotations in
`org.apache.fory.json.annotation`:
`JsonAnyGetter`, `JsonAnyProperty`, `JsonAnySetter`, `JsonBase64`, `JsonCodec`, `JsonCreator`, `JsonFormat`,
`JsonAnyGetter`, `JsonAnyProperty`, `JsonAnySetter`, `JsonByteArray`, `JsonCodec`, `JsonCreator`, `JsonFormat`,
`JsonIgnore`, `JsonProperty`, `JsonPropertyOrder`, `JsonRawValue`, `JsonSubTypes`, `JsonUnwrapped`,
`JsonValidator`, and `JsonValue`. `JsonType` is a separate build-time model marker. They are
Fory JSON APIs, not Jackson, Gson, or Fory binary-protocol compatibility annotations.
Expand Down Expand Up @@ -356,28 +356,36 @@ Any-property features are independent.
as a trusted raw root value. That combination is serialization-only: the ordinary one-String
`JsonCreator` cannot turn an input object or array into a String.

## `JsonBase64`
## `JsonByteArray`

`JsonBase64` selects a quoted standard Base64 JSON string for one exact `byte[]` field or getter:
Unannotated `byte[]` values use quoted standard Base64 JSON strings. `JsonByteArray` selects
`BASE64` or `ARRAY` for one exact `byte[]` field or getter, in both reading and writing:

```java
import org.apache.fory.json.annotation.JsonBase64;
import org.apache.fory.json.annotation.JsonByteArray;

public final class Attachment {
@JsonBase64
@JsonByteArray(JsonByteArray.Format.ARRAY)
public byte[] numbers;

@JsonByteArray(JsonByteArray.Format.BASE64)
public byte[] content;
}
```

Bytes `{1, 2, 3}` are written as `{"content":"AQID"}` and decoded back to the original array.
Fory writes the Base64 characters directly to the JSON output and decodes directly from the JSON
input without creating an intermediate String. Standard Base64 padding is preserved. Java null
follows the property's normal inclusion rule and reads from JSON null as null.
For bytes `{1, -2, 3}`, `numbers` is written as `[1,-2,3]` and `content` as `"Af4D"`.
`ARRAY` reads JSON arrays using the signed byte range `[-128, 127]`; `BASE64` reads standard
Base64 strings and preserves padding when writing. Each representation also accepts JSON null,
and null output follows the property's normal inclusion rule. The default Base64 codec does not
accept numeric-array input; select `ARRAY` for a property that uses that format.

The format is required when the annotation is present. It applies only to the annotated byte-array
property, not to container elements or map values. Mixin declarations can select or remove it.
It cannot share a logical property with `JsonRawValue`, an occurrence `JsonCodec`, `JsonFormat`,
or an Any declaration. Conflicting formats on the field and getter of one property are rejected.

The annotation is not a type-use annotation and does not change ordinary unannotated `byte[]`
properties, container elements, or Map values. It cannot share a logical property with
`JsonRawValue`, an occurrence `JsonCodec`, `JsonFormat`, or an Any declaration. The equivalent explicit codec is
`@JsonCodec(Base64ByteArrayCodec.class)`.
Base64 values are binary leaves excluded from the graph-memory budget. Numeric arrays count their
array storage against that budget; see [Security](security.md#depth-and-graph-memory-limits).

## `JsonFormat`

Expand Down Expand Up @@ -439,7 +447,7 @@ unwrapped values are intentionally rejected. Types with ambiguous formatting sem
legacy and SQL date types, `Duration`, `Period`, `TimeZone`, `ZoneId`, and `ZoneOffset`, are not
supported. A wrapper with a complete registered, annotation-selected, polymorphic, or `JsonValue`
representation is also rejected because that representation owns the whole wrapper.
`JsonFormat` cannot share a field with `JsonCodec`, `JsonBase64`, `JsonRawValue`, `JsonAnyProperty`,
`JsonFormat` cannot share a field with `JsonCodec`, `JsonByteArray`, `JsonRawValue`, `JsonAnyProperty`,
`JsonUnwrapped`, or `JsonValue`.

## `JsonUnwrapped`
Expand Down
6 changes: 3 additions & 3 deletions docs/json/graalvm.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,10 @@ 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. `JsonFormat` date/time fields use the same direct-field,
getters support trusted raw String values, and `JsonByteArray` fields and getters select Base64 strings or numeric
byte arrays as on the JVM. `JsonFormat` date/time fields use the same direct-field,
one-wrapper-level, and `timezone` behavior as on the JVM. For direct target annotations, annotate
each reachable owning model with `JsonType` so Native Image retains these members and the Base64
each reachable owning model with `JsonType` so Native Image retains these members and the selected byte-array
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
Expand Down
2 changes: 1 addition & 1 deletion docs/json/kotlin.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ their normal Fory JSON representation when used from Kotlin:
| text | `String`, exact `CharSequence`, `StringBuilder`, and `StringBuffer` use String shapes |
| arbitrary/reduced-precision number | `BigInteger`, `BigDecimal`, Fory `Float16`, and `BFloat16` use their core numeric shapes and limits |
| enum | quoted enum constant name |
| Java/Kotlin arrays | normal JSON arrays; `ByteArray` is numeric unless `JsonBase64` selects binary; unsigned semantic arrays are listed below |
| Java/Kotlin arrays | normal JSON arrays except `ByteArray`, which uses Base64 strings by default; `@field:JsonByteArray(JsonByteArray.Format.ARRAY)` selects numeric arrays; unsigned semantic arrays are listed below |
| Optional and atomic | `Optional<T>`, primitive Optionals, atomic scalars/references, and atomic arrays keep their transparent core shapes subject to the nullability rules above |
| quoted JDK values | `Currency`, `File`, `URI`, `Path`, `Pattern`, `UUID`, `Locale`, `Charset`, and `TimeZone` keep their core String shapes |
| legacy date/time | `Date`, `Calendar`, and available `java.sql.Date`, `Time`, and `Timestamp` keep their epoch-millisecond shapes |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
import org.apache.fory.json.annotation.JsonAnyGetter;
import org.apache.fory.json.annotation.JsonAnyProperty;
import org.apache.fory.json.annotation.JsonAnySetter;
import org.apache.fory.json.annotation.JsonBase64;
import org.apache.fory.json.annotation.JsonByteArray;
import org.apache.fory.json.annotation.JsonCodec;
import org.apache.fory.json.annotation.JsonCreator;
import org.apache.fory.json.annotation.JsonFormat;
Expand Down Expand Up @@ -489,6 +489,20 @@ private static void testValueAnnotations() {
new String(json.toJsonBytes(raw), StandardCharsets.UTF_8).equals("{\"body\":{\"id\":1}}"));
Preconditions.checkArgument(
json.fromJson("{\"body\":\"text\"}", RawValue.class).body.equals("text"));
ArrayBytes arrayBytes = new ArrayBytes();
arrayBytes.value = new byte[] {1, -2, 3};
Preconditions.checkArgument(json.toJson(arrayBytes).equals("{\"value\":[1,-2,3]}"));
Preconditions.checkArgument(
new String(json.toJsonBytes(arrayBytes), StandardCharsets.UTF_8)
.equals("{\"value\":[1,-2,3]}"));
Preconditions.checkArgument(
Arrays.equals(
json.fromJson("{\"value\":[1,-2,3]}", ArrayBytes.class).value, arrayBytes.value));
Preconditions.checkArgument(
Arrays.equals(
json.fromJson("{\"value\":[1,-2,3]}".getBytes(StandardCharsets.UTF_8), ArrayBytes.class)
.value,
arrayBytes.value));
Base64Bytes base64Bytes = new Base64Bytes();
base64Bytes.value = new byte[] {1, 2, 3};
Preconditions.checkArgument(json.toJson(base64Bytes).equals("{\"value\":\"AQID\"}"));
Expand Down Expand Up @@ -1273,9 +1287,16 @@ public static final class RawValue {
@JsonRawValue public String body;
}

@JsonType
public static final class ArrayBytes {
@JsonByteArray(JsonByteArray.Format.ARRAY)
public byte[] value;
}

@JsonType
public static final class Base64Bytes {
@JsonBase64 public byte[] value;
@JsonByteArray(JsonByteArray.Format.BASE64)
public byte[] value;
}

@JsonType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public object PlatformCorpusChecks {
verifyRoot(decoded)
val text = json.toJson(decoded, type)
check(text.contains("\"display_label\":\"mixin\""))
check(text.contains("\"numbers\":[1,-2,3]"))
check(text.contains("\"binary\":\"Af4D\""))
check(text.contains("\"defaultBytes\":\"Af4D\""))
verifyRoot(json.fromJson(text, type))
verifyRoot(json.fromJson(json.toJsonBytes(decoded, type), type))
}
Expand All @@ -43,5 +46,8 @@ public object PlatformCorpusChecks {
check(actual.profile.label == expected.profile.label)
check(actual.token == expected.token)
check(actual.box == expected.box)
check(actual.numbers.contentEquals(expected.numbers))
check(actual.binary.contentEquals(expected.binary))
check(actual.defaultBytes.contentEquals(expected.defaultBytes))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package org.apache.fory.integration.kotlin.json.corpus

import kotlin.jvm.JvmInline
import org.apache.fory.json.annotation.JsonByteArray
import org.apache.fory.json.annotation.JsonCodec
import org.apache.fory.json.annotation.JsonMixin
import org.apache.fory.json.annotation.JsonSubTypes
Expand Down Expand Up @@ -104,6 +105,11 @@ public data class PlatformRoot(
public val profile: PlatformJavaProfile,
@field:JsonCodec(PlatformTokenCodec::class) public val token: PlatformToken,
public val box: PlatformBox<String>,
@field:JsonByteArray(JsonByteArray.Format.ARRAY)
public val numbers: ByteArray = byteArrayOf(1, -2, 3),
@get:JsonByteArray(JsonByteArray.Format.BASE64)
public val binary: ByteArray = byteArrayOf(1, -2, 3),
public val defaultBytes: ByteArray = byteArrayOf(1, -2, 3),
)

internal fun platformRootValue(): PlatformRoot =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,14 @@ public class KspRetentionResourceTest {
assertTrue(sealed.contains("class $PACKAGE.PlatformSquare"), sealed)
assertTrue(sealed.contains("class $PACKAGE.PlatformOpen"), sealed)
assertFalse(sealed.contains("class $PACKAGE.PlatformOpenDescendant"), sealed)
assertConstructor(rules("PlatformRoot"), "$PACKAGE.PlatformTokenCodec")
val root = rules("PlatformRoot")
assertConstructor(root, "$PACKAGE.PlatformTokenCodec")
assertTrue(
root.contains("@interface org.apache.fory.json.annotation.JsonByteArray"),
root,
)
assertConstructor(root, "org.apache.fory.json.codec.Base64ByteArrayCodec")
assertConstructor(root, "org.apache.fory.json.codec.ArrayCodec\$SignedByteArrayCodec")
assertConstructor(
rules("PlatformDirectOverride"),
"$PACKAGE.PlatformDirectOverrideCodec",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ final class JsonMixinAnnotations {
private static final String JSON_ANY_GETTER = JSON_PACKAGE + ".annotation.JsonAnyGetter";
private static final String JSON_ANY_PROPERTY = JSON_PACKAGE + ".annotation.JsonAnyProperty";
private static final String JSON_ANY_SETTER = JSON_PACKAGE + ".annotation.JsonAnySetter";
private static final String JSON_BASE64 = JSON_PACKAGE + ".annotation.JsonBase64";
private static final String JSON_BYTE_ARRAY = JSON_PACKAGE + ".annotation.JsonByteArray";
private static final String JSON_CODEC = JSON_PACKAGE + ".annotation.JsonCodec";
private static final String JSON_CREATOR = JSON_PACKAGE + ".annotation.JsonCreator";
private static final String JSON_FORMAT = JSON_PACKAGE + ".annotation.JsonFormat";
Expand All @@ -73,7 +73,7 @@ final class JsonMixinAnnotations {
JSON_ANY_GETTER,
JSON_ANY_PROPERTY,
JSON_ANY_SETTER,
JSON_BASE64,
JSON_BYTE_ARRAY,
JSON_CODEC,
JSON_CREATOR,
JSON_FORMAT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ final class JsonTypeProcessor {
private static final String JSON_PROPERTY = JSON_PACKAGE + ".annotation.JsonProperty";
private static final String JSON_VALUE = JSON_PACKAGE + ".annotation.JsonValue";
private static final String JSON_RAW_VALUE = JSON_PACKAGE + ".annotation.JsonRawValue";
private static final String JSON_BASE64 = JSON_PACKAGE + ".annotation.JsonBase64";
private static final String JSON_BYTE_ARRAY = JSON_PACKAGE + ".annotation.JsonByteArray";
private static final String JSON_UNWRAPPED = JSON_PACKAGE + ".annotation.JsonUnwrapped";
private static final String JSON_VALIDATOR = JSON_PACKAGE + ".annotation.JsonValidator";
private static final String BASE64_CODEC = JSON_PACKAGE + ".codec.Base64ByteArrayCodec";
Expand Down Expand Up @@ -569,8 +569,13 @@ private void collectMixinTargets(JsonMixinAnnotations annotations, Model model)
private void collectOccurrenceCodec(
JsonMixinAnnotations annotations, Element element, Model model) {
collectCodecAnnotation(annotationMirror(annotations, element, JSON_CODEC), model);
if (hasAnnotation(annotations, element, JSON_BASE64)) {
model.codecTypes.add(BASE64_CODEC);
AnnotationMirror byteArray = annotationMirror(annotations, element, JSON_BYTE_ARRAY);
if (byteArray != null) {
VariableElement format = (VariableElement) annotationValue(byteArray, "value").getValue();
model.codecTypes.add(
format.getSimpleName().contentEquals("ARRAY")
? JSON_PACKAGE + ".codec.ArrayCodec$SignedByteArrayCodec"
: BASE64_CODEC);
}
}

Expand Down Expand Up @@ -1134,7 +1139,7 @@ private boolean isJsonMethod(
|| hasAnnotation(annotations, method, JSON_CODEC)
|| hasAnnotation(annotations, method, JSON_VALUE)
|| hasAnnotation(annotations, method, JSON_RAW_VALUE)
|| hasAnnotation(annotations, method, JSON_BASE64)
|| hasAnnotation(annotations, method, JSON_BYTE_ARRAY)
|| hasAnnotation(annotations, method, JSON_VALIDATOR)
|| hasJsonAnnotations(annotations, method.getParameters())) {
return true;
Expand Down
Loading
Loading