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
11 changes: 6 additions & 5 deletions docs/guide/java/android-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,12 @@ The same exact-rule approach supports every `JsonCodec` member; it is not limite
codecs. `JsonType` is not required for codec selection on an ordinary class.

For `@JsonType` models, the generated R8 rules also retain `JsonValue` fields and effective methods,
fixed `JsonRawValue` and `JsonBase64` fields and getters, their runtime annotations, and the Base64
codec constructor. Without `@JsonType`, these 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.
fixed `JsonRawValue` and `JsonBase64` fields and getters, `JsonFormat` date/time fields, their runtime
annotations, and the Base64 codec constructor. Without `@JsonType`, these 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. `JsonFormat` keeps the same direct-field
and one-wrapper-level behavior as on the JVM.

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
Expand Down
10 changes: 6 additions & 4 deletions docs/guide/java/graalvm-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,12 @@ 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.
`byte[]` values as on the JVM. `JsonFormat` date/time fields use the same direct-field and
one-wrapper-level 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 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
Expand Down
67 changes: 59 additions & 8 deletions docs/guide/java/json-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ disabled. Every other builder option keeps the behavior described above.

Fory JSON provides `JsonProperty`, `JsonPropertyOrder`, `JsonIgnore`, `JsonAnyProperty`,
`JsonAnyGetter`, `JsonAnySetter`, `JsonCreator`, `JsonCodec`, `JsonValue`, `JsonRawValue`,
`JsonBase64`, `JsonUnwrapped`, and `JsonSubTypes` as mapping annotations under
`JsonBase64`, `JsonFormat`, `JsonUnwrapped`, and `JsonSubTypes` as mapping annotations under
`org.apache.fory.json.annotation`. `JsonType` is a separate build-time generation marker. They are
not Jackson, Gson, or Fory binary-protocol annotations.

Expand All @@ -321,6 +321,7 @@ import org.apache.fory.json.annotation.JsonAnySetter;
import org.apache.fory.json.annotation.JsonBase64;
import org.apache.fory.json.annotation.JsonCodec;
import org.apache.fory.json.annotation.JsonCreator;
import org.apache.fory.json.annotation.JsonFormat;
import org.apache.fory.json.annotation.JsonIgnore;
import org.apache.fory.json.annotation.JsonMixin;
import org.apache.fory.json.annotation.JsonMixinRemove;
Expand Down Expand Up @@ -379,7 +380,7 @@ that the subclass inherits, but the resulting annotation applies only while that
mapped.

All Fory JSON mapping annotations are supported: `JsonAnyGetter`, `JsonAnyProperty`,
`JsonAnySetter`, `JsonBase64`, `JsonCodec`, `JsonCreator`, `JsonIgnore`, `JsonProperty`,
`JsonAnySetter`, `JsonBase64`, `JsonCodec`, `JsonCreator`, `JsonFormat`, `JsonIgnore`, `JsonProperty`,
`JsonPropertyOrder`, `JsonRawValue`, `JsonSubTypes`, `JsonUnwrapped`, and `JsonValue`. `JsonType`
cannot be added or removed because it controls build-time generation rather than the JSON schema.

Expand All @@ -404,7 +405,8 @@ abstract class QuotedMessageMixin {

Removal affects only the matched declaration in the exact-target configuration. Removing
`JsonRawValue` restores ordinary quoted String output; removing `JsonBase64` restores the ordinary
`byte[]` representation; removing `JsonUnwrapped` restores a nested object property. Type-level
`byte[]` representation; removing `JsonFormat` restores the ordinary date/time representation;
removing `JsonUnwrapped` restores a nested object property. Type-level
removal can mask inherited `JsonCodec` or `JsonPropertyOrder` declarations for the exact target.
Removing an absent annotation is harmless, but the selector must still match exactly one target
declaration. A source cannot both declare and remove the same annotation type on one declaration.
Expand Down Expand Up @@ -612,7 +614,7 @@ Java null follows the property's existing inclusion rule and is written as JSON
Reading is unchanged and still expects a JSON string. A raw object or array written through the
property cannot be read back into that `String`. The annotation does not apply to setters, creator
parameters, Any declarations, container elements, or Map values, and it cannot share an occurrence
with `JsonCodec`.
with `JsonCodec` or `JsonFormat`.
As an occurrence-local representation, it keeps the raw String shape even when the value type has
an exact builder-registered codec.

Expand Down Expand Up @@ -641,9 +643,58 @@ normal inclusion rule.

The annotation is not a type-use annotation and does not affect ordinary `byte[]` properties,
container elements, or Map values. It cannot share a logical property with `JsonRawValue`, an
occurrence `JsonCodec`, or an Any declaration. The equivalent explicit codec is
occurrence `JsonCodec`, `JsonFormat`, or an Any declaration. The equivalent explicit codec is
`@JsonCodec(Base64ByteArrayCodec.class)`.

### `JsonFormat`

Use `JsonFormat` on a date/time field to select its JSON text pattern in both directions. Patterns
use `DateTimeFormatter` syntax and the root locale:

```java
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.fory.json.annotation.JsonFormat;

public final class Schedule {
@JsonFormat(pattern = "dd/MM/uuuu")
public LocalDate day;

@JsonFormat(pattern = "dd/MM/uuuu")
public Optional<LocalDate> optionalDay;

@JsonFormat(pattern = "dd/MM/uuuu")
public List<LocalDate> days;

@JsonFormat(pattern = "dd/MM/uuuu")
public Map<String, LocalDate> daysByName;
}
```

For `day = LocalDate.of(2024, 1, 2)`, the property is written as `"day":"02/01/2024"` and
the same text reads back to that date. The annotation applies to the field value when it is a
supported date/time type. For one direct wrapper, it applies to an array or collection element, an
`AtomicReferenceArray` element, an `Optional` or `AtomicReference` content value, or a Map value.
This includes `List`, `Set`, and their concrete `Collection` implementations. Null handling still
follows the property's ordinary inclusion rule.

Supported values are exact `LocalDate`, `LocalTime`, `LocalDateTime`, `Instant`, `ZonedDateTime`,
`Year`, `YearMonth`, `MonthDay`, `OffsetTime`, `OffsetDateTime`, `HijrahDate`, `JapaneseDate`,
`MinguoDate`, and `ThaiBuddhistDate` types. `Instant` uses UTC; zoned and offset types use the zone or
offset carried by the value. The pattern must contain enough information to reconstruct the
declared type.

`JsonFormat` is a field annotation, not a type-use annotation. A record component works through its
generated field. Nested wrappers, Map keys, raw or wildcard direct children, JSON Any values, and
unwrapped values are intentionally rejected. Types with ambiguous formatting semantics, including
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`,
`JsonUnwrapped`, or `JsonValue`.

### `JsonUnwrapped`

Use `JsonUnwrapped` when an object-valued property should keep its Java object boundary but place
Expand Down Expand Up @@ -689,7 +740,7 @@ Fory rejects duplicate final names, recursive chains made only of unwrapped prop
parameterized children, JSON Any children, polymorphic or custom-codec child roots, and scalar,
array, collection, or Map children. Flatten Maps with `JsonAnyProperty`, `JsonAnyGetter`, or
`JsonAnySetter`. An unwrapped property cannot use `JsonProperty.value`, a non-default
`JsonProperty.include`, or `JsonCodec`; ordinary leaf properties inside the child keep their normal
`JsonProperty.include`, `JsonCodec`, or `JsonFormat`; ordinary leaf properties inside the child keep their normal
annotations.

### Dynamic object members
Expand Down Expand Up @@ -1166,8 +1217,8 @@ default. URL and arbitrary unsupported Number/CharSequence subclasses require ex
- No InputStream parser, incremental `OutputStream` writer on the `ForyJson` root API, or
pretty-print configuration.
- No Jackson/Gson annotation compatibility.
- No aliases, views, filters, injection, managed/back references, object identity annotations, root
wrapping, or format annotations.
- No aliases, views, filters, injection, managed/back references, object identity annotations, or
root wrapping.
- Fory core's `Expose` is ignored.

Circular graphs eventually fail `maxDepth`; they are not reconstructed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ final class JsonMixinAnnotations {
private static final String JSON_BASE64 = JSON_PACKAGE + ".annotation.JsonBase64";
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";
private static final String JSON_IGNORE = JSON_PACKAGE + ".annotation.JsonIgnore";
private static final String JSON_PROPERTY = JSON_PACKAGE + ".annotation.JsonProperty";
private static final String JSON_PROPERTY_ORDER = JSON_PACKAGE + ".annotation.JsonPropertyOrder";
Expand All @@ -74,6 +75,7 @@ final class JsonMixinAnnotations {
JSON_BASE64,
JSON_CODEC,
JSON_CREATOR,
JSON_FORMAT,
JSON_IGNORE,
JSON_PROPERTY,
JSON_PROPERTY_ORDER,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -799,6 +800,66 @@ public void encodedCreatorPipeline() throws Exception {
new Object[] {new byte[] {1, 2, 3}});
}

@Test
public void formatRecordPipeline() throws Exception {
assumeJava16Source();
CompilationResult result =
compile(
"test.FormatRecord",
"package test;\n"
+ "import java.time.LocalDate;\n"
+ "import org.apache.fory.json.annotation.*;\n"
+ "@JsonType public record FormatRecord(\n"
+ " @JsonFormat(pattern = \"dd/MM/uuuu\") LocalDate value) {}\n");
assertTrue(result.success, result.diagnostics());
String rules = result.generatedResource(RULE_PREFIX + "test.FormatRecord.pro");
assertTrue(rules.contains("@interface org.apache.fory.json.annotation.JsonFormat"), rules);
ClassLoader loader = result.classLoader();
Class<?> type = loader.loadClass("test.FormatRecord");
Object value = type.getConstructor(LocalDate.class).newInstance(LocalDate.of(2024, 1, 2));
for (ForyJson json : jsonRuntimes(loader)) {
assertEquals(json.toJson(value), "{\"value\":\"02/01/2024\"}");
Object decoded = json.fromJson("{\"value\":\"03/01/2024\"}", type);
assertEquals(type.getMethod("value").invoke(decoded), LocalDate.of(2024, 1, 3));
}
}

@Test
public void formatMixinPipeline() throws Exception {
CompilationResult result =
compile(
"test.FormatTarget",
"package test;\n"
+ "import java.time.LocalDate;\n"
+ "import org.apache.fory.json.annotation.*;\n"
+ "public final class FormatTarget { public LocalDate value; }\n"
+ "@JsonMixin(target = FormatTarget.class) abstract class FormatMixin {\n"
+ " @JsonFormat(pattern = \"dd/MM/uuuu\") LocalDate value;\n"
+ "}\n");
assertTrue(result.success, result.diagnostics());
String base = "FormatMixin_ForyJsonMixin_test_x2e_FormatTarget";
assertTrue(result.hasGeneratedSource("test/" + base + "_ForyJsonCodec.java"));
String rules = result.generatedResource(MIXIN_RULE_PREFIX + "test.FormatMixin.pro");
assertTrue(rules.contains("@interface org.apache.fory.json.annotation.JsonFormat"), rules);
ClassLoader loader = result.classLoader();
Class<?> target = loader.loadClass("test.FormatTarget");
Class<?> mixin = loader.loadClass("test.FormatMixin");
Object value = target.getConstructor().newInstance();
target.getField("value").set(value, LocalDate.of(2024, 1, 2));
for (boolean codegen : new boolean[] {false, true}) {
ForyJson json =
ForyJson.builder()
.withCodegen(codegen)
.withAsyncCompilation(false)
.withClassLoader(loader)
.registerMixin(mixin)
.build();
assertEquals(json.toJson(value), "{\"value\":\"02/01/2024\"}");
Object decoded = json.fromJson("{\"value\":\"03/01/2024\"}", target);
assertEquals(target.getField("value").get(decoded), LocalDate.of(2024, 1, 3));
}
}

@Test
public void missingCompanionFails() throws Exception {
CompilationResult result =
Expand Down
59 changes: 54 additions & 5 deletions java/fory-json/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ automatically disabled. Every other builder option keeps the behavior described
## JSON annotations

Fory JSON provides these mapping annotations in `org.apache.fory.json.annotation`:
`JsonAnyGetter`, `JsonAnyProperty`, `JsonAnySetter`, `JsonBase64`, `JsonCodec`, `JsonCreator`,
`JsonAnyGetter`, `JsonAnyProperty`, `JsonAnySetter`, `JsonBase64`, `JsonCodec`, `JsonCreator`, `JsonFormat`,
`JsonIgnore`, `JsonProperty`, `JsonPropertyOrder`, `JsonRawValue`, `JsonSubTypes`, `JsonUnwrapped`,
and `JsonValue`. `JsonType` is a separate build-time generation marker. They are Fory JSON APIs,
not Jackson, Gson, or Fory binary-protocol compatibility annotations.
Expand Down Expand Up @@ -711,9 +711,58 @@ follows the property's normal inclusion rule and reads from JSON null as null.

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`, or an Any declaration. The equivalent explicit codec is
`JsonRawValue`, an occurrence `JsonCodec`, `JsonFormat`, or an Any declaration. The equivalent explicit codec is
`@JsonCodec(Base64ByteArrayCodec.class)`.

### `JsonFormat`

Use `JsonFormat` on a date/time field to select its JSON text pattern in both directions. Patterns
use `DateTimeFormatter` syntax and the root locale:

```java
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.fory.json.annotation.JsonFormat;

public final class Schedule {
@JsonFormat(pattern = "dd/MM/uuuu")
public LocalDate day;

@JsonFormat(pattern = "dd/MM/uuuu")
public Optional<LocalDate> optionalDay;

@JsonFormat(pattern = "dd/MM/uuuu")
public List<LocalDate> days;

@JsonFormat(pattern = "dd/MM/uuuu")
public Map<String, LocalDate> daysByName;
}
```

For `day = LocalDate.of(2024, 1, 2)`, the property is written as `"day":"02/01/2024"` and
the same text reads back to that date. The annotation applies to the field value when it is a
supported date/time type. For one direct wrapper, it applies to an array or collection element, an
`AtomicReferenceArray` element, an `Optional` or `AtomicReference` content value, or a Map value.
This includes `List`, `Set`, and their concrete `Collection` implementations. Null handling still
follows the property's ordinary inclusion rule.

Supported values are exact `LocalDate`, `LocalTime`, `LocalDateTime`, `Instant`, `ZonedDateTime`,
`Year`, `YearMonth`, `MonthDay`, `OffsetTime`, `OffsetDateTime`, `HijrahDate`, `JapaneseDate`,
`MinguoDate`, and `ThaiBuddhistDate` types. `Instant` uses UTC; zoned and offset types use the zone or
offset carried by the value. The pattern must contain enough information to reconstruct the
declared type.

`JsonFormat` is a field annotation, not a type-use annotation. A record component works through its
generated field. Nested wrappers, Map keys, raw or wildcard direct children, JSON Any values, and
unwrapped values are intentionally rejected. Types with ambiguous formatting semantics, including
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`,
`JsonUnwrapped`, or `JsonValue`.

### `JsonUnwrapped`

Use `JsonUnwrapped` to place an object-valued property's members directly in the containing JSON
Expand Down Expand Up @@ -759,7 +808,7 @@ before dynamic Any handling.
Fory rejects duplicate final names, recursive chains made only of unwrapped properties,
parameterized children, JSON Any children, polymorphic or custom-codec child roots, and scalar,
array, collection, or Map children. Use `JsonAnyProperty`, `JsonAnyGetter`, or `JsonAnySetter` to
flatten a Map. `JsonProperty.value`, non-default `JsonProperty.include`, and `JsonCodec` are not
flatten a Map. `JsonProperty.value`, non-default `JsonProperty.include`, `JsonCodec`, and `JsonFormat` are not
valid on an unwrapped property; ordinary child leaf properties may still use them.

### Dynamic object members
Expand Down Expand Up @@ -1298,8 +1347,8 @@ Jackson object mapping:
- no `InputStream` parser or incremental `OutputStream` writer on the `ForyJson` root API;
- no pretty-print configuration;
- no Jackson/Gson annotation compatibility layer;
- no aliases, views, filters, injection, managed/back references, object identity annotations,
root wrapping, or format annotations;
- no aliases, views, filters, injection, managed/back references, object identity annotations, or
root wrapping;
- no Fory core `Expose` processing.

Circular graphs eventually fail `maxDepth`; they are not reconstructed. Use Fory core's binary
Expand Down
Loading
Loading