blob: 343ed46e8ff75972ac3f1202e72013dd3d9945bc [file] [view]
---
title: JSON Support
sidebar_position: 19
id: json_support
license: |
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.
---
Fory JSON is Apache Fory's thread-safe Java JSON codec. It supports Java objects, records,
immutable creator-based classes, common JDK types, generic containers, custom complete-value
codecs, and finite annotation-declared polymorphism through interpreted and runtime-generated
codecs.
Fory JSON is separate from Fory's binary native and xlang protocols. Use JSON for interoperable
text payloads such as HTTP APIs, browser traffic, logs, and configuration. Use the binary protocol
when you need reference identity, circular graphs, cross-language schema metadata, or Fory's
binary-only features.
## Requirements and installation
The module targets Java 8 bytecode. Record mapping requires Java 17 or later.
Fory JSON is currently available from the source tree as `1.4.0-SNAPSHOT`. Until a published Fory
release contains the module, install it locally from the repository root:
```bash
cd java
mvn -pl fory-json -am -DskipTests install
```
Maven:
```xml
<dependency>
<groupId>org.apache.fory</groupId>
<artifactId>fory-json</artifactId>
<version>1.4.0-SNAPSHOT</version>
</dependency>
```
Gradle, using `mavenLocal()` for the snapshot:
```kotlin
implementation("org.apache.fory:fory-json:1.4.0-SNAPSHOT")
```
Keep all Fory modules on the same version. Replace the snapshot with the released version that
contains `fory-json` after publication.
### JDK 25 and later
Open `java.lang.invoke` to Fory core. On the classpath:
```bash
--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
```
On the module path:
```bash
--add-opens=java.base/java.lang.invoke=org.apache.fory.core
```
Fory JSON's JPMS module name is `org.apache.fory.json`.
## Quick start
Create one `ForyJson` instance and reuse it. It is thread-safe and has no close lifecycle.
```java
import java.nio.charset.StandardCharsets;
import org.apache.fory.json.ForyJson;
public final class JsonExample {
private static final ForyJson JSON = ForyJson.builder().build();
public static final class User {
public long id;
public String name;
public User() {}
User(long id, String name) {
this.id = id;
this.name = name;
}
}
public static void main(String[] args) {
User input = new User(7, "Alice");
String text = JSON.toJson(input);
byte[] utf8 = JSON.toJsonBytes(input);
User fromText = JSON.fromJson(text, User.class);
User fromUtf8 = JSON.fromJson(utf8, User.class);
System.out.println(text);
System.out.println(new String(utf8, StandardCharsets.UTF_8));
System.out.println(fromText.name + " / " + fromUtf8.name);
}
}
```
Unknown input properties are skipped unless a read-enabled Any field or any-setter receives them.
Null object properties are omitted by default. Default JSON member discovery order is not a
compatibility contract; use `JsonPropertyOrder` or `JsonProperty.index` when emitted member order
must be explicit.
## Reading and writing
Fory JSON supports String and UTF-8 byte input/output. There is no `InputStream` parsing API.
| Operation | Runtime type | Declared `Class` | Declared `TypeRef` |
| -------------------- | ------------------------- | ------------------------------- | ---------------------------------- |
| String output | `toJson(value)` | `toJson(value, type)` | `toJson(value, typeRef)` |
| UTF-8 bytes | `toJsonBytes(value)` | `toJsonBytes(value, type)` | `toJsonBytes(value, typeRef)` |
| UTF-8 `OutputStream` | `writeJsonTo(value, out)` | `writeJsonTo(value, type, out)` | `writeJsonTo(value, typeRef, out)` |
| String input | - | `fromJson(text, type)` | `fromJson(text, typeRef)` |
| UTF-8 input | - | `fromJson(bytes, type)` | `fromJson(bytes, typeRef)` |
Parsing consumes exactly one value and rejects trailing non-whitespace. String and byte-array
outputs are detached from internal buffers.
`writeJsonTo` buffers the complete document, performs one `OutputStream.write`, and neither flushes
nor closes the stream. It is not incremental streaming. I/O failures are wrapped in
`ForyJsonException`.
### Generic and declared types
Use `TypeRef` for generic roots:
```java
import java.util.List;
import org.apache.fory.reflect.TypeRef;
TypeRef<List<User>> usersType = new TypeRef<List<User>>() {};
List<User> users = json.fromJson("[{\"id\":7,\"name\":\"Alice\"}]", usersType);
String encoded = json.toJson(users, usersType);
```
Typed writes require fully bound types and reject wildcards and type variables. Values must be
assignable to the declared raw type. The declared schema controls serialization, including nested
generic element types and closed subtype metadata.
Use a declared base type when it owns `JsonSubTypes`:
```java
Shape shape = new Circle(2);
json.toJson(shape); // Concrete runtime representation
json.toJson(shape, Shape.class); // Configured Shape subtype representation
```
For `List<Shape>`, use `new TypeRef<List<Shape>>() {}` so each element retains the declared subtype
schema.
## Thread safety and code generation
`ForyJson` is immutable and thread-safe after `build()`. Registered and annotation-selected
`JsonValueCodec` instances and type checkers are shared and must also be thread-safe.
Code generation and asynchronous compilation are enabled by default. Disable them for diagnostics
or environments that prohibit runtime compilation:
```java
ForyJson json =
ForyJson.builder()
.withCodegen(false)
.withAsyncCompilation(false)
.build();
```
`withConcurrencyLevel` controls reusable operation states, not a caller limit. Extra concurrent
operations use temporary state rather than one global lock.
## Object mapping
Default discovery merges members with the same Java logical property name:
- eligible instance fields across the hierarchy, regardless of Java visibility;
- public non-static `getX()` and boolean `isX()` getters;
- public non-static void `setX(value)` setters.
Static, transient, synthetic, and `Class<?>` fields are excluded. Class-valued accessors and
`getClass()` are excluded. An annotation on an ineligible member fails rather than becoming a
silent no-op.
An ordinary final field is a write source but not a mutable read sink. Use a record, `JsonCreator`,
or custom codec for immutable construction. Records use their canonical constructor.
Enable field-only discovery with:
```java
ForyJson json = ForyJson.builder().withFieldMode(true).build();
```
In field mode, getter/setter annotations are invalid. For ordinary properties, unknown members are
skipped and duplicate members use the last value. Polymorphic discriminator members are stricter
and must occur exactly once. JSON null is rejected for primitive targets. Most reference targets
return null, but a selected built-in or custom codec may define another result; declared Optional
targets return the corresponding empty Optional.
An ordinary class with a no-argument constructor runs it before readable properties are assigned,
so missing properties retain values established by field initializers or the constructor. On an
ordinary JVM, a class without such a constructor is allocated without running its constructors or
field initializers, and missing properties retain JVM zero or null values. Android cannot construct
an ordinary class without a usable no-argument constructor. GraalVM native image on JDK 25 and
later also requires one for most ordinary classes; the supported exception is a `Serializable`
class whose first non-serializable superclass is `Object`.
For portable construction, use a record, `JsonCreator`, or a no-argument constructor. Do not use an
ordinary constructor as a deserialization completion hook: property assignment follows a
no-argument constructor, while constructor-bypassing paths do not run it.
## Supported Java types
| Group | Types and behavior |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Scalars | Primitive and boxed booleans/numbers/chars, strings and string builders, `BigInteger`, `BigDecimal`, Fory half-precision numbers, enums |
| Containers | Primitive/boxed/object arrays; collection, list, set, queue, deque, blocking, sorted, and navigable interfaces; map, sorted-map, navigable-map, and concurrent-map interfaces; supported concrete implementations; `EnumSet`; `EnumMap`; `Optional` variants; atomic values and arrays |
| Time | `Date`, `Calendar`, `TimeZone`, Java time types, supported chronology dates, optional `java.sql.Date`, `Time`, and `Timestamp` |
| Other JDK | `UUID`, `URI`, `File`, `Path`, `Locale`, `Charset`, `Currency`, `Pattern`, `BitSet`, `ByteBuffer` |
| Optional libraries | Guava `ImmutableList`, `ImmutableSet`, `ImmutableSortedSet`, `ImmutableMap`, `ImmutableBiMap`, `ImmutableSortedMap`, and `ImmutableIntArray` when Guava is present |
| Objects | Mutable concrete classes, records, creator classes, `JsonObject`, `JsonArray` |
Interfaces are reconstructed with appropriate standard mutable implementations. `ArrayBlockingQueue`,
`Arrays.asList` results, JDK immutable collections, empty/singleton/unmodifiable wrappers,
constructor-constrained implementations, and unlisted Guava immutable implementations cannot be
reconstructed. Guava remains optional.
Non-finite float/double values use quoted `"NaN"`, `"Infinity"`, and `"-Infinity"` tokens.
### Built-in representations
| Java type | JSON representation |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Enum | Constant name string |
| `Date`, `Calendar`, `java.sql.Date`, `Time`, `Timestamp` | Epoch-millisecond number |
| `TimeZone` | Time-zone ID string |
| Java time and supported chronology date types | Standard textual string |
| `UUID`, `URI`, `File`, `Path`, `Locale`, `Charset`, `Currency`, `Pattern` | Type-specific text string; `File` and `Path` use path text, `Locale` uses a language tag, and `Pattern` loses flags |
| `BitSet` | Array of signed `long` words |
| `ByteBuffer` | Array of signed byte values from position to limit |
| Optional and atomic wrappers | Contained scalar, array, or value directly |
`Calendar` is reconstructed as a new `GregorianCalendar`, so its original subtype, time zone, and
other configuration are not retained. A null Optional reference and an empty Optional both write
JSON null; reading JSON null as a declared Optional type returns the corresponding empty Optional.
### Dynamic JSON trees
Reading `Object.class` produces natural JSON values:
| JSON | Java |
| ------------------- | ------------------------------------------ |
| Object | `JsonObject` |
| Array | `JsonArray` |
| String/boolean/null | `String`, `Boolean`, null |
| Integer | `Long`, or `BigInteger` outside long range |
| Fraction/exponent | `Double` |
`JsonObject` preserves insertion order and `JsonArray` is mutable.
### Map keys
Declared keys support String, byte, short, int, long, their boxed types, and enums. `Object` keys
can write String, number, boolean, character, and enum values, but read back as strings. Null keys
are rejected.
## Builder configuration
| Method | Default | Effect |
| ---------------------------- | -------------------------------------------- | ------------------------------------------------------ |
| `writeNullFields` | `false` | Default null-property inclusion |
| `withCodegen` | `true` | Generated object codecs |
| `withAsyncCompilation` | `true` | Asynchronous generated-code compilation |
| `withFieldMode` | `false` | Field-only discovery when true |
| `withPropertyNamingStrategy` | `LOWER_CAMEL_CASE` | Naming of properties without explicit names |
| `withClassLoader` | Snapshotted context loader, then Fory loader | Resolve annotation subtype class names |
| `maxDepth` | `20` | Maximum nested object/array depth |
| `withConcurrencyLevel` | `max(1, 2 * processors)` | Reusable operation-state count |
| `withBufferSizeLimitBytes` | 2 MiB | Reusable capacity retained by each pooled writer |
| `registerCodec` | None | Exact-class complete-value codec |
| `withTypeChecker` | None | Application policy in addition to Fory's disallow list |
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.
## Annotations
Fory JSON provides `JsonProperty`, `JsonPropertyOrder`, `JsonIgnore`, `JsonAnyProperty`,
`JsonAnyGetter`, `JsonAnySetter`, `JsonCreator`, `JsonCodec`, and `JsonSubTypes` under
`org.apache.fory.json.annotation`. They are not Jackson, Gson, or Fory binary-protocol annotations.
```java
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.fory.json.PropertyNamingStrategy;
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.JsonCodec;
import org.apache.fory.json.annotation.JsonCreator;
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;
```
### `JsonProperty`
An annotation on a field, getter, or setter configures the complete merged logical property:
```java
@JsonProperty("user_id")
private long id;
@JsonProperty(include = JsonProperty.Include.ALWAYS)
private String displayName;
@JsonProperty(index = 10)
private String email;
```
Supported inclusion values are:
- `DEFAULT`: inherit `writeNullFields`;
- `ALWAYS`: include null;
- `NON_NULL`: omit null.
`index` controls relative serialization order. Indexed properties are written in ascending index
order before unindexed properties. Indexes must be non-negative, may contain gaps, and must be
unique among writable properties. `-1` means unspecified; lower values are invalid. An index on a
setter-only, creator-only, or write-ignored property is invalid.
Inclusion affects writing only. Identical repeated declarations are allowed; conflicting explicit
names, indexes, or non-default policies fail. Two properties cannot normalize to the same JSON
name. `JsonProperty` cannot be combined with an Any logical property or declared on a
`JsonAnySetter`. `NON_EMPTY`, aliases, and formatting are unsupported.
### `JsonPropertyOrder`
Use `JsonPropertyOrder` to combine a named prefix, property indexes, and final-name alphabetic
ordering:
```java
@JsonPropertyOrder(value = {"id", "display_name"}, alphabetic = true)
public final class User {
@JsonProperty(index = 20)
public String name;
@JsonProperty(value = "display_name", index = 10)
public String displayName;
public long id;
public int age;
public String address;
}
```
The output order is `id`, `display_name`, `name`, `address`, then `age`:
```json
{ "id": 1, "display_name": "Alice", "name": "alice", "address": "x", "age": 30 }
```
The named prefix is written first. Remaining indexed properties follow in ascending index order.
When `alphabetic = true`, remaining unindexed properties are sorted by final JSON name; otherwise
they keep their existing relative order. Use `@JsonPropertyOrder(alphabetic = true)` when no named
prefix is needed. Alphabetic comparison uses Java's natural, case-sensitive String order and does
not depend on the locale.
Order entries match the final JSON name first and the Java logical property name second. This lets
`display_name` match an explicit `JsonProperty` name while an unannotated `displayName` can still be
addressed by either `display_name` under `SNAKE_CASE` or its Java name `displayName`.
The list may be empty only when `alphabetic` is true. Its entries must be non-empty, unique writable
properties; unknown and duplicate entries fail when the object metadata is built. A subclass
declaration replaces both settings from its superclass as a whole; declarations are not merged. If
the subclass has no declaration, the nearest superclass declaration is used and resolved against the
subclass properties. Interface declarations are not considered.
Property order affects serialization only. Deserialization remains name-based and accepts members
in any order. Subtype discriminators remain before user properties.
A write-enabled `JsonAnyProperty` or `JsonAnyGetter` participates as one position identified by its
Java logical property name:
```java
@JsonPropertyOrder({"id", "properties", "timestamp"})
public final class Event {
public String id;
@JsonAnyProperty
public Map<String, Object> properties;
public long timestamp;
}
```
The position emits every `properties` entry in Map iteration order between `id` and `timestamp`; it
does not emit a member named `properties`. Naming strategies do not transform the Any ordering
name. Input-only Any fields and `JsonAnySetter` have no write position. Dynamic keys cannot appear
in `JsonPropertyOrder`, and alphabetic ordering never sorts entries inside the Map.
### Naming strategy
```java
ForyJson json =
ForyJson.builder()
.withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.build();
```
The default `LOWER_CAMEL_CASE` preserves the discovered Java logical property name. `SNAKE_CASE`
maps `userName` to `user_name`, `URLValue` to `url_value`, and `version2FA` to `version2_fa`.
Explicit `JsonProperty` names, parameter-local creator names, and subtype discriminator properties
bypass the strategy. Dynamic Any keys also bypass it.
### `JsonIgnore`
`JsonIgnore` is field-targeted and controls both directions of the complete logical property:
```java
@JsonIgnore(ignoreRead = false, ignoreWrite = true)
private String serverManagedValue;
```
Both flags default to true. Accessors cannot restore an ignored direction, and `JsonProperty`
cannot override it. Fory core's `Expose` has no effect in Fory JSON.
### Dynamic object members
Use `JsonAnyProperty` to flatten a `Map<String, V>` field into the containing JSON object and store
otherwise unknown input members:
```java
public final class Event {
public String id;
@JsonAnyProperty
public Map<String, Object> properties = new LinkedHashMap<>();
}
```
For `properties` containing `"source" -> "mobile"`, the result contains `"source":"mobile"`
beside `id`; no nested `properties` member is written. The field reads and writes by default.
`JsonIgnore` may select one direction, but it cannot disable both. During reading, Fory reuses an
existing Map or initializes a null non-final field on the first unknown member. A readable final
field on an ordinary mutable object must already contain a mutable Map. Records and property-list
`JsonCreator` types instead receive the accumulated Map through their construction argument.
Use `JsonAnyGetter` and `JsonAnySetter` for method-backed writing and reading:
```java
public final class Event {
private final Map<String, Object> properties = new LinkedHashMap<>();
@JsonAnyGetter
public Map<String, Object> getProperties() {
return properties;
}
@JsonAnySetter
public void putProperty(String name, Object value) {
properties.put(name, value);
}
}
```
An any-getter must be a public instance method with no arguments and a `Map<String, V>` return type.
An any-setter must be a public instance method with signature `void method(String, V)`. Either may
be used independently. When paired, their resolved value types must match after primitive types are
boxed. A primitive any-setter value rejects JSON null. Any-setters are not supported on records or
types using `JsonCreator`.
A read-enabled `JsonAnyProperty` on a record component supplies that component from unknown input.
In property-list creator mode, a read-enabled Any field must correspond to one listed creator
argument; parameter-local creator mode cannot bind it. A write-only Any field or any-getter cannot
occupy a creator argument. If one claims a record component, that component receives its normal
Java default during reading.
An any-getter claims its complete Java logical property: both `getProperties()` and `properties()`
claim `properties`, so same-named ordinary fields and accessors are not mapped again as a fixed
member. Fory does not infer a differently named backing field; use `JsonIgnore` if that field must
not be mapped separately. An any-setter has no logical property name and does not claim a field.
The logical name is used only for grouping and `JsonPropertyOrder`; it is not a fixed JSON member.
An input member with that name is a dynamic entry rather than a nested aggregate. The same output
key remains valid unless another fixed property conflicts with it.
One effective type hierarchy may use either one Any field or up to one any-getter and one
any-setter. Field-backed and method-backed forms cannot be mixed, and method annotations are invalid
in field mode. An unannotated override disables an inherited method annotation. `JsonProperty` is
invalid on an any-setter and on every member claimed by an Any field or getter. A same-named field
cannot use `JsonIgnore` to suppress an any-getter's write direction, and its `ignoreRead` flag does
not disable a separate any-setter.
Dynamic keys are emitted unchanged in Map iteration order. A null Map emits nothing, while a null
Map value emits JSON null regardless of fixed-property null settings. Null and non-String output
keys are rejected. Raw Maps, wildcard or unresolved keys, and non-String key types are invalid.
Declared fixed members, including members excluded from reading, are not delivered to an Any
input. Output keys whose Fory field-name hash conflicts with a fixed property are rejected,
including differently spelled hash collisions. Fory does not inspect an Any Map for a key whose
name or Fory field-name hash conflicts with an inline subtype discriminator. An exact-name output
key emits a duplicate JSON member; on input, a differently spelled hash collision is classified as
the discriminator by the child field table. Applications must keep dynamic keys distinct from the
active discriminator by both name and hash. Repeated unknown names replace the Map value; an
any-setter is called for every occurrence. Fixed input lookup is also hash-based, so a differently
spelled colliding name follows the fixed member instead of Any handling. Escaped input names are
decoded before delivery.
### `JsonCreator`
The compact mode lists existing Java logical property names in parameter order and reuses their
normalized metadata:
```java
public final class User {
public final long id;
public final String name;
@JsonCreator({"id", "name"})
public User(long id, String name) {
this.id = id;
this.name = name;
}
}
```
Parameter-local mode gives every parameter an explicit JSON name and permits creator-only inputs:
```java
@JsonCreator
public static User create(
@JsonProperty("user_id") long id,
@JsonProperty("display_name") String name) {
return new User(id, name);
}
```
The modes cannot be mixed. Compact names must be non-empty and unique, their count must match the
parameter count, and compact parameters cannot also declare `JsonProperty`. Parameter-local mode
requires a non-empty, unique `JsonProperty` name on every parameter. The creator is the complete
read schema and setters do not run after it.
Exactly one creator is allowed. It must be public, have at least one parameter, and be neither
varargs nor generic. A factory is also static, declares the target class as its exact return type,
and returns a non-null value whose runtime class is exactly the target. Missing references use null,
missing primitives use zero, duplicate members use the last value, and explicit primitive null
fails. Records cannot declare `JsonCreator`.
### `JsonSubTypes`
`JsonSubTypes` defines a complete finite table on an interface or abstract base. Each entry has a
case-sensitive logical name and exactly one Java source: a class literal or trusted binary
`className`. JSON never supplies class names or expands the table.
Default property inclusion:
```java
@JsonSubTypes(
property = "kind",
value = {
@JsonSubTypes.Type(value = Circle.class, name = "circle"),
@JsonSubTypes.Type(
className = "com.example.shape.Rectangle",
name = "rectangle")
})
public interface Shape {}
```
```json
{ "kind": "circle", "radius": 2 }
```
The discriminator is emitted first but may appear at any direct input member position. It must
occur exactly once, contain a known String name, and not collide with a subtype property.
Wrapper object:
```java
@JsonSubTypes(
inclusion = JsonSubTypes.Inclusion.WRAPPER_OBJECT,
value = {@JsonSubTypes.Type(value = Circle.class, name = "circle")})
public interface Shape {}
```
```json
{ "circle": { "radius": 2 } }
```
Wrapper array:
```java
@JsonSubTypes(
inclusion = JsonSubTypes.Inclusion.WRAPPER_ARRAY,
value = {@JsonSubTypes.Type(value = Circle.class, name = "circle")})
public interface Shape {}
```
```json
["circle", { "radius": 2 }]
```
| Inclusion | `property` | Value rule |
| ---------------- | ---------------------- | ----------------------------------- |
| `PROPERTY` | Required and non-empty | Ordinary subtype object members |
| `WRAPPER_OBJECT` | Must be empty | Complete subtype value |
| `WRAPPER_ARRAY` | Must be empty | Complete subtype value as element 1 |
Wrapper inclusions support exact custom subtype codecs. Logical names and resolved concrete,
assignable classes must each be unique. Only exact listed runtime classes are accepted; listing a
parent does not admit descendants. The annotation is read from the declared base itself and is not
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`.
## Custom codecs
`JsonValueCodec<T>` reads and writes one complete JSON value, including null, through Fory's
concrete String/UTF-8 writers and Latin-1/UTF-16/UTF-8 readers. It is a direct streaming-value SPI,
not a JSON abstract syntax tree (AST) codec. It never handles Map keys; `MapKeyCodec` owns JSON
object member names.
Implement all five representations with the same JSON shape:
```java
import java.math.BigDecimal;
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;
public final class MoneyCodec implements JsonValueCodec<Money> {
@Override
public void writeString(StringJsonWriter writer, Money value) {
if (value == null) {
writer.writeNull();
} else {
writer.writeBigDecimal(value.amount);
}
}
@Override
public void writeUtf8(Utf8JsonWriter writer, Money value) {
if (value == null) {
writer.writeNull();
} else {
writer.writeBigDecimal(value.amount);
}
}
@Override
public Money readLatin1(Latin1JsonReader reader) {
return reader.tryReadNullToken() ? null : new Money(reader.readBigDecimal());
}
@Override
public Money readUtf16(Utf16JsonReader reader) {
return reader.tryReadNullToken() ? null : new Money(reader.readBigDecimal());
}
@Override
public Money readUtf8(Utf8JsonReader reader) {
return reader.tryReadNullToken() ? null : new Money(reader.readBigDecimal());
}
}
final class Money {
final BigDecimal amount;
Money(BigDecimal amount) {
this.amount = amount;
}
}
```
Register it once:
```java
import org.apache.fory.json.ForyJson;
ForyJson json =
ForyJson.builder()
.registerCodec(Money.class, new MoneyCodec())
.build();
```
The parent property still controls its name, ignore direction, and null inclusion before the codec
runs. An emitted property and every array, collection, map-value, Optional, or atomic-reference
position delegate the complete value, including null. The registered codec instance is shared
concurrently and must be thread-safe. A custom codec on a subtype is compatible with wrapper
inclusion, not inline property inclusion. A codec on the base replaces its `JsonSubTypes`
annotation.
### `JsonCodec` declarations and type uses
Use `@JsonCodec` on a class, record, enum, or interface to make a codec the default representation
for that declaration and its descendants:
```java
@JsonCodec(MoneyCodec.class)
public final class Money {}
@JsonCodec(AccountCodec.class)
public interface Account {}
public final class RetailAccount implements Account {}
```
The default applies at root `Class` and raw `TypeRef` targets and at unannotated nested value
positions. Fory explicitly inherits declarations through superclasses and interfaces; it does not
use Java `@Inherited`.
Use the same annotation at a type-use position to select a codec for exactly that occurrence:
```java
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
public final class Invoice {
public @JsonCodec(MoneyCodec.class) Money total;
public List<@JsonCodec(MoneyCodec.class) Money> items;
public Set<@JsonCodec(MoneyCodec.class) Money> uniqueItems;
public Collection<@JsonCodec(MoneyCodec.class) Money> allItems;
public Map<String, @JsonCodec(MoneyCodec.class) Money> byName;
public Optional<@JsonCodec(MoneyCodec.class) Money> optional;
public AtomicReference<@JsonCodec(MoneyCodec.class) Money> current;
// Codec for each element.
public @JsonCodec(MoneyCodec.class) Money[] itemArray;
// Codec for the complete array value.
public Money @JsonCodec(MoneyArrayCodec.class) [] encodedArray;
// Codec for each complete inner Set value.
public List<@JsonCodec(MoneySetCodec.class) Set<Money>> groups;
// Normal containers with a codec only at the leaf.
public List<Set<@JsonCodec(MoneyCodec.class) Money>> nestedItems;
}
```
Every array dimension is a distinct type-use node. This recursive model also covers
multidimensional arrays, `AtomicReferenceArray`, concrete containers, and generic container
subclasses. Fory follows the inherited `Collection<E>` or `Map<K,V>` binding instead of assuming a
fixed type-argument index.
One logical property's field, effective getter, effective setter, record component, and matching
creator parameter contribute to one resolved codec tree. Missing annotations are not conflicts;
equal codec classes at one node merge, while different classes fail with both sources and the
nested path. Type-use metadata survives generic substitution, including a codec attached to a
bound type argument used by a field declared as a type variable. Supported positions include field
and record-component types, getter return types, setter value parameters, and `JsonCreator`
constructor or factory value parameters.
`TypeRef` preserves ordinary Java generic types but not arbitrary occurrence annotations. A root
`TypeRef<List<@JsonCodec(...) Money>>` therefore cannot carry that local override. Put it on a
discovered model property, declare a default on `Money`, or use exact builder registration.
### Codec precedence
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 |
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.
### Deterministic inheritance and conflicts
For inherited declarations, Fory collects every annotated proper superclass and interface, then
removes a candidate when another declaring type is its Java subtype. The remaining most-specific
declarations resolve as follows:
- one declaration wins;
- unrelated declarations naming the same codec class are consistent;
- unrelated declarations naming different codec classes conflict.
This is independent of `implements` order and reflection order. A child interface declaration
overrides its parent declaration, and a child class declaration overrides its parent:
```java
@JsonCodec(ParentCodec.class)
interface Parent {}
@JsonCodec(ChildCodec.class)
interface Child extends Parent {}
final class Value implements Child {} // ChildCodec
@JsonCodec(BaseCodec.class)
class Base {}
@JsonCodec(ValueCodec.class)
final class Concrete extends Base {} // ValueCodec
```
Unrelated interfaces using a common codec are valid; different codecs are ambiguous:
```java
@JsonCodec(CommonCodec.class)
interface A {}
@JsonCodec(CommonCodec.class)
interface B {}
final class Consistent implements A, B {}
@JsonCodec(ACodec.class)
interface Left {}
@JsonCodec(BCodec.class)
interface Right {}
final class Ambiguous implements Left, Right {} // Metadata error.
```
Superclass/interface candidates follow the same subtype rule:
| Declaring-type relationship | Codec classes | Result |
| ----------------------------------------------- | ----------------- | --------------------------------------------------------- |
| Superclass implements or inherits the interface | Same or different | Superclass declaration wins because it is more specific |
| Superclass and interface are unrelated | Same | Common codec is accepted |
| Superclass and interface are unrelated | Different | Conflict; class does not automatically override interface |
Disambiguate by annotating the concrete class, annotating only the current type use, or registering
an exact codec instance:
```java
@JsonCodec(ConcreteCodec.class)
final class DeclaredValue implements Left, Right {}
final class Holder {
public @JsonCodec(LocalCodec.class) Ambiguous value;
}
ForyJson json =
ForyJson.builder()
.registerCodec(Ambiguous.class, new ConcreteCodec())
.build();
```
Fory retains Jackson's useful idea of explicitly inheriting annotations through class and
interface hierarchies, but rejects traversal-order first-wins behavior. Java subtype specificity
and codec-class identity decide the result; incomparable different codecs produce an error instead
of depending on hierarchy declaration order.
### Nested composition and Map keys
A custom codec owns its complete current value and cannot delegate annotated children. An explicit
nested type-use under an outer custom codec is therefore rejected as hidden configuration:
```java
// Invalid: LocalCodec cannot run because CustomListCodec owns the whole list.
public @JsonCodec(CustomListCodec.class)
List<@JsonCodec(LocalCodec.class) Money> values;
```
A child class's declaration codec is only a lazy default. The outer codec prevents child lookup,
so that default is not a hidden explicit instruction and is valid:
```java
@JsonCodec(MoneyCodec.class)
final class DeclaredMoney {}
public @JsonCodec(CustomListCodec.class) List<DeclaredMoney> values;
```
The hidden-explicit-descendant rule also applies when the outer complete codec comes from builder
registration or a class/interface declaration. Without an outer custom codec, arrays, collections,
map values, Optional, and atomic references resolve their annotated children normally.
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
declarations are not supported JSON model targets.
### Construction, inherited results, and platform support
An annotation codec class must be public, concrete, top-level or static nested, and have a public
no-argument constructor. One successfully constructed instance is shared by all annotated sites
and concurrent operations of the built `ForyJson`; it must be thread-safe. Use
`registerCodec(Target.class, instance)` when the codec needs configuration.
In a named Java module, export or open the codec's package to `org.apache.fory.json`. Fory does not
bypass a closed module boundary and reports an actionable access error.
When a parent or interface declaration supplies the codec for a more specific target, every reader
result must be null or assignable to that target. A parent codec may return the requested child and
succeed. Returning a plain parent while decoding the child fails with `ForyJsonException` that
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.
## Type validation and untrusted input
Fory JSON always applies its fixed disallow list. Add an application policy with:
```java
ForyJson json =
ForyJson.builder()
.withTypeChecker(
(className, context) ->
className.startsWith("com.example.model.")
|| className.equals("java.util.List")
|| className.equals("java.util.Map"))
.build();
```
Allow every application model and non-built-in container type that the declared schema uses. The
checker applies while application types are prepared for serialization and parsing and must be
thread-safe. Built-in scalars normally skip the custom checker, but selecting an application codec
for a built-in target makes that target subject to the checker. Custom codecs cannot bypass the
fixed disallow list.
`withClassLoader` fixes subtype `className` resolution. Otherwise `build()` snapshots the thread
context class loader and falls back to the Fory JSON loader.
`maxDepth` is not an input-size or memory quota. Enforce request size, timeout, and resource limits
at the transport boundary. `Class`, `URL`, `InetAddress`, and `InetSocketAddress` are unsupported by
default. URL and arbitrary unsupported Number/CharSequence subclasses require exact custom codecs.
## Limits and unsupported features
- No shared-reference identity or circular-reference protocol. Use Fory binary when needed.
- No open polymorphism, JSON class-name IDs, subtype discovery, or runtime subtype-table extension.
- 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, format annotations, or annotation-driven raw JSON values.
- Fory core's `Expose` is ignored.
Circular graphs eventually fail `maxDepth`; they are not reconstructed.
## Errors and troubleshooting
| Symptom | Action |
| ---------------------------------- | ----------------------------------------------------------------------------------------------- |
| `ForyJsonException` | Check JSON grammar, target type, mapping support, depth, trailing content, or output cause |
| `InsecureException` | Check Fory's disallow list and the configured type checker |
| Builder `IllegalArgumentException` | Use positive depth, concurrency, and retained-buffer values |
| Declared write fails | Remove wildcard/type variables and pass an assignable value; primitive declarations reject null |
| Immutable value is empty | Use a record, valid creator, or custom codec |
| Ordinary object cannot be created | Add a usable no-argument constructor, use a record or creator, or register a codec |
| Ordinary accessor annotation fails | Use an eligible public JavaBean accessor and disable field mode |
| Any annotation fails | Use one field form or one valid method pair with resolved `Map<String, V>` types |
| Codec annotation fails | Resolve same-node or hierarchy conflicts, hidden nested overrides, or codec constructor access |
| Subtype fails | Write with the declared base, list the exact runtime class, and use the configured wire shape |
| Collection fails | Target a supported interface/common implementation or register a codec |
Creator failures other than `Error` are wrapped with their original cause. User codec code may
still throw its own runtime exceptions.
## Related Java guides
- [Java serialization overview](index.md)
- [Native binary serialization](native-serialization.md)
- [Xlang binary serialization](xlang-serialization.md)
- [Java configuration](configuration.md)
- [Troubleshooting](troubleshooting.md)