blob: bbda440c32b379b5eebd5d4842d0269ef2d9d335 [file] [view]
# Fory JSON
Fory JSON is Apache Fory's thread-safe Java JSON codec. It provides interpreted and
runtime-generated codecs for Java objects, records, immutable creator-based classes, common JDK
types, generic containers, custom complete-value codecs, and finite annotation-declared
polymorphism.
Fory JSON is a separate data format from Fory's binary native and xlang protocols. Use it when a
system must exchange ordinary JSON with browsers, APIs, logs, configuration, or another JSON
implementation. Use the Fory binary protocol when you need cross-language schema metadata,
reference identity, circular graphs, or Fory's binary-only features.
## Performance
The benchmark compares fory-json with Jackson and Gson using String and UTF-8 byte APIs. The String
group excludes UTF-8 conversion. Gson's byte results include its required String/UTF-8 conversion.
Higher throughput is better.
<p align="center">
<img src="../../docs/benchmarks/java/json/string_throughput.png" width="49%" alt="Java JSON String throughput">
<img src="../../docs/benchmarks/java/json/utf8_bytes_throughput.png" width="49%" alt="Java JSON UTF-8 bytes throughput">
</p>
| Representation | Operation | fory-json ops/sec | Jackson ops/sec | Gson ops/sec |
| -------------- | ----------- | ----------------: | --------------: | -----------: |
| String | Serialize | 7,387,465 | 2,049,368 | 1,084,042 |
| String | Deserialize | 2,897,955 | 1,074,885 | 902,772 |
| UTF-8 bytes | Serialize | 10,375,498 | 1,868,614 | 1,037,211 |
| UTF-8 bytes | Deserialize | 3,077,158 | 1,268,397 | 933,079 |
See the [full benchmark report](../../docs/benchmarks/java/json/).
## Requirements and installation
Fory JSON supports Java 8 and later on standard JDKs, GraalVM native images, and Android. Java
records are supported on Java 17 and later.
Fory JSON is available from Maven Central.
Maven:
```xml
<dependency>
<groupId>org.apache.fory</groupId>
<artifactId>fory-json</artifactId>
<version>1.4.0</version>
</dependency>
```
Gradle:
```kotlin
implementation("org.apache.fory:fory-json:1.4.0")
```
Use the same version for every Fory module in one application.
### JDK 25 and later
On JDK 25 and later, open `java.lang.invoke` to Fory core. For a classpath application:
```bash
--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
```
For a module-path application:
```bash
--add-opens=java.base/java.lang.invoke=org.apache.fory.core
```
The JPMS module name of Fory JSON is `org.apache.fory.json`.
## Quick start
Create one `ForyJson` instance and reuse it. The instance 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 property discovery order is not a
compatibility contract; use `JsonPropertyOrder` or `JsonProperty.index` when emitted property order
must be explicit.
## Reading and writing APIs
Fory JSON supports String input/output and UTF-8 byte input/output. It does not currently provide an
`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)` |
Every `fromJson` call consumes exactly one JSON value and rejects trailing non-whitespace content.
Returned Strings and byte arrays are detached from internal reusable buffers.
`writeJsonTo` buffers the complete UTF-8 document, performs one `OutputStream.write`, and neither
flushes nor closes the caller-owned stream. It is an output convenience API, not incremental JSON
streaming. I/O failures are wrapped in `ForyJsonException`.
### Generic types
Use `TypeRef` whenever a root type contains generic arguments:
```java
import java.util.List;
import org.apache.fory.json.ForyJson;
import org.apache.fory.reflect.TypeRef;
ForyJson json = ForyJson.builder().build();
TypeRef<List<User>> usersType = new TypeRef<List<User>>() {};
List<User> users = json.fromJson("[{\"id\":7,\"name\":\"Alice\"}]", usersType);
String encoded = json.toJson(users, usersType);
```
Declared writes require a fully bound type. Wildcards and type variables are rejected. A non-null
value must be assignable to the declared raw type.
The declared schema controls serialization. For example, a property declared as a concrete parent
class uses the parent's mapped properties rather than automatically adding subclass-only fields. A
declared `Object` value uses runtime dispatch when writing and natural JSON mapping when reading.
### Declared types and polymorphism
The no-type write overloads dispatch from the runtime class. Use a declared-type overload when a
base type owns `JsonSubTypes` metadata:
```java
Shape shape = new Circle(2);
json.toJson(shape); // Circle's concrete representation
json.toJson(shape, Shape.class); // Shape's configured subtype representation
json.toJsonBytes(shape, Shape.class);
json.writeJsonTo(shape, Shape.class, outputStream);
```
For containers of polymorphic values, carry the declared base type in `TypeRef`:
```java
TypeRef<List<Shape>> shapesType = new TypeRef<List<Shape>>() {};
String encoded = json.toJson(shapes, shapesType);
```
## Thread safety, reuse, and code generation
`ForyJson` is immutable and thread-safe after `build()`. Reuse one instance instead of creating a
builder and runtime for every operation. Registered and annotation-selected `JsonValueCodec`
instances and the `JsonTypeChecker` may be called concurrently and must also be thread-safe.
Code generation and asynchronous compilation are enabled by default. Disabling code generation is
useful for diagnostics or environments that prohibit runtime compilation:
```java
ForyJson json =
ForyJson.builder()
.withCodegen(false)
.withAsyncCompilation(false)
.build();
```
`withConcurrencyLevel` configures the number of reusable operation states, not a maximum number of
concurrent callers. When all reusable states are busy, Fory JSON creates a temporary state rather
than serializing callers through one global lock.
## Java object mapping
### Default property discovery
By default, Fory JSON builds one logical property from members with the same Java property name:
- eligible instance fields across the class hierarchy, including private, protected,
package-private, and public fields;
- public non-static JavaBean getters named `getX()`;
- public non-static boolean getters named `isX()`;
- public non-static void setters named `setX(value)`.
Static, transient, synthetic, and `Class<?>` fields are excluded. `getClass()` and accessors whose
value type is `Class<?>` are also excluded. An annotation placed on an ineligible member is rejected
instead of being silently ignored.
An ordinary final field can be written but is not used as a mutable read sink. Use a record,
`JsonCreator`, or a custom codec for immutable construction.
### Field mode
Field mode disables getter and setter discovery while retaining eligible fields:
```java
ForyJson json = ForyJson.builder().withFieldMode(true).build();
```
Annotations on methods are invalid in field mode because those methods are not part of the JSON
property model.
### Construction and input behavior
Fory JSON supports ordinary concrete classes, Java records, and classes with an explicit
`JsonCreator` constructor or factory.
- Records use their canonical constructor.
- Creator-based classes use only the declared creator read schema and do not run setters afterward.
- Unknown object members are skipped.
- An ordinary class with a no-argument constructor runs that constructor before readable
properties are assigned. Missing properties therefore retain values established by field
initializers or that constructor.
- On an ordinary JVM, a class without a no-argument constructor is allocated without running its
constructors or field initializers. Its missing properties retain JVM zero or null values.
- Creator reference parameters default to null and creator primitive parameters default to zero.
- Duplicate ordinary properties use the last value. A polymorphic discriminator is stricter and
must appear 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; for example, declared `Optional` targets
return `Optional.empty()`.
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 a
portable construction contract, use a record, `JsonCreator`, or a no-argument constructor. Do not
use ordinary-constructor side effects as a deserialization completion hook: when a no-argument
constructor runs, property assignment happens afterward, and constructor-bypassing paths do not run
it at all.
## Supported Java types
The following groups have built-in mappings. Exact wire representations are stable JSON values, but
application schemas should still declare the intended Java type when precision or construction
matters.
| Group | Supported types and behavior |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Core scalars | `boolean`, numeric primitives, `char`, their boxed types, `String`, `CharSequence`, `StringBuilder`, `StringBuffer` |
| Numbers | `Number`, `BigInteger`, `BigDecimal`, Fory `Float16` and `BFloat16`, `AtomicInteger`, `AtomicLong` |
| Enums | Enum constant names as JSON strings |
| Arrays | Primitive arrays, boxed arrays, String arrays, object arrays, and multidimensional arrays |
| Collections | `Collection`, `List`, `Set`, `Queue`, deque, blocking, sorted, and navigable interfaces; their abstract bases; `EnumSet`; and concrete implementations with an accessible no-argument constructor |
| Maps | `Map`, sorted, navigable, and concurrent interfaces; `AbstractMap`; `EnumMap`; and concrete implementations with an accessible no-argument constructor |
| Optional and atomic | `Optional`, `OptionalInt`, `OptionalLong`, `OptionalDouble`, `AtomicBoolean`, `AtomicReference`, and atomic arrays |
| Time | `Date`, `Calendar`, `TimeZone`, `LocalDate`, `LocalTime`, `LocalDateTime`, `Instant`, `Duration`, `ZoneOffset`, `ZoneId`, `ZonedDateTime`, `Year`, `YearMonth`, `MonthDay`, `Period`, `OffsetTime`, `OffsetDateTime`, and supported chronology dates |
| Other JDK types | `UUID`, `URI`, `File`, `Path`, `Locale`, `Charset`, `Currency`, `Pattern`, `BitSet`, `ByteBuffer` |
| Optional modules | `java.sql.Date`, `Time`, and `Timestamp`; Guava `ImmutableList`, `ImmutableSet`, `ImmutableSortedSet`, `ImmutableMap`, `ImmutableBiMap`, `ImmutableSortedMap`, and `ImmutableIntArray` when Guava is present |
| Objects | Mutable concrete classes, records, creator-based classes, `JsonObject`, and `JsonArray` |
Collection interfaces are reconstructed with standard mutable implementations, such as
`ArrayList`, `LinkedHashSet`, `ArrayDeque`, `LinkedBlockingQueue`, `LinkedBlockingDeque`, or
`TreeSet`, according to the declared interface. Map interfaces similarly use `LinkedHashMap`,
`TreeMap`, `ConcurrentHashMap`, or `ConcurrentSkipListMap`. `ArrayBlockingQueue`, `Arrays.asList`
results, JDK immutable collections, empty/singleton/unmodifiable wrappers, constructor-constrained
implementations, and unlisted Guava immutable implementations cannot be reconstructed. Guava
support is optional and does not make Guava a required runtime dependency.
Non-finite float and double values use the quoted strings `"NaN"`, `"Infinity"`, and
`"-Infinity"`. Use explicit `BigInteger` or `BigDecimal` targets when arbitrary precision must be
preserved.
### Built-in representations
These built-in values use the following ordinary JSON shapes:
| Java type | JSON representation |
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Enum | Constant name as a string |
| `Date`, `Calendar`, `java.sql.Date`, `Time`, `Timestamp` | Epoch milliseconds as a number |
| `TimeZone` | Time-zone ID as a string |
| Java time and supported chronology date types | Their standard textual form as a string |
| `UUID`, `URI`, `File`, `Path`, `Locale`, `Charset`, `Currency`, `Pattern` | Type-specific text as a string; `File` and `Path` use path text, `Locale` uses a language tag, and `Pattern` does not retain flags |
| `BitSet` | Array of signed `long` words from `BitSet.toLongArray()` |
| `ByteBuffer` | Array of signed byte values for the remaining range from position to limit |
| Optional and atomic wrappers | Their contained scalar, array, or value directly |
`Calendar` reads epoch milliseconds into a new `GregorianCalendar`; its original calendar subtype,
time zone, and other configuration are not retained. A null `Optional` reference and an empty
`Optional` both write JSON null, and JSON null read as a declared Optional type becomes the
corresponding empty Optional.
### Dynamic JSON trees
Reading as `Object` uses natural JSON values:
| JSON value | Java value |
| --------------------------- | ------------ |
| Object | `JsonObject` |
| Array | `JsonArray` |
| String | `String` |
| Boolean | `Boolean` |
| Integer within `long` range | `Long` |
| Larger integer | `BigInteger` |
| Fraction or exponent | `Double` |
| Null | `null` |
`JsonObject` preserves member insertion order and `JsonArray` is mutable. They can also be created
and written directly.
```java
import org.apache.fory.json.JsonArray;
import org.apache.fory.json.JsonObject;
JsonObject object = new JsonObject();
JsonArray items = new JsonArray();
items.add(1);
items.add("two");
object.put("items", items);
String encoded = json.toJson(object);
```
### Map keys
JSON object member names are strings. Declared map keys support `String`, `byte`, `short`, `int`,
`long`, their boxed forms, and enums. A map declared with `Object` keys can write String, number,
boolean, character, and enum keys, but reads them back as strings because JSON does not retain the
original key type. Null map keys are rejected.
## Builder configuration
| Builder method | Default | User-visible effect |
| -------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------- |
| `writeNullFields(boolean)` | `false` | Default inclusion of null object properties |
| `withCodegen(boolean)` | `true` | Enable generated object codecs |
| `withAsyncCompilation(boolean)` | `true` | Compile generated codecs asynchronously |
| `withFieldMode(boolean)` | `false` | When true, discover fields without getters/setters |
| `withPropertyNamingStrategy(strategy)` | `LOWER_CAMEL_CASE` | Name properties without an explicit `JsonProperty` name |
| `withClassLoader(loader)` | Snapshotted thread context loader, then Fory JSON loader | Resolve annotation-declared subtype class names |
| `maxDepth(int)` | `20` | Maximum nested object/array depth for reads and writes |
| `withMaxCachedFieldNames(int)` | `DEFAULT_MAX_CACHED_FIELD_NAMES` (`8192`) | Field-name cache entries per reader; zero disables caching |
| `withConcurrencyLevel(int)` | `max(1, 2 * processors)` | Number of reusable concurrent operation states |
| `withBufferSizeLimitBytes(int)` | 2 MiB | Maximum reusable capacity retained by each pooled writer |
| `registerCodec(type, codec)` | None | Replace the exact class's complete JSON codec |
| `registerMixin(mixinType)` | None | Apply one annotation Mixin to its exact declared target |
| `withTypeChecker(checker)` | No custom checker | Apply an application type policy in addition to Fory's disallow list |
Depth, concurrency level, and buffer retention limit must be positive. The cached-field-name limit
applies independently to each reader. It does not limit accepted JSON input; zero disables this
cache. The buffer retention setting does not limit JSON input or output size; it only limits
reusable writer storage retained after an operation. Apply request/body size limits at the
transport boundary when parsing untrusted input.
Builder mutation after `build()` does not modify an existing `ForyJson` runtime.
On Android and in a GraalVM native image, runtime code generation and asynchronous compilation are
automatically disabled. Every other builder option keeps the behavior described above.
## JSON annotations
Fory JSON provides these mapping annotations in `org.apache.fory.json.annotation`:
`JsonAnyGetter`, `JsonAnyProperty`, `JsonAnySetter`, `JsonBase64`, `JsonCodec`, `JsonCreator`,
`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.
`JsonType` asks the annotation processor to generate direct property and creator operations plus
exact retention rules. It is not inherited, so annotate each eligible concrete model that needs a
generated companion. A directly annotated `JsonValue` Record also receives a companion for its
value accessor and canonical constructor. Ordinary unannotated classes may still use reflection; on
Android they need application-authored exact R8 rules. Android-desugared Records require
processor-generated operations from either a direct `JsonType` declaration or a compiled exact
`JsonMixin` pair. A directly annotated model that uses the default object codec fails during codec
creation if its generated companion is missing.
See the [GraalVM guide](../../docs/guide/java/graalvm-support.md) and
[Android guide](../../docs/guide/java/android-support.md) for the platform workflows.
### Mixins
Use a JSON Mixin to apply Fory JSON mapping annotations to a class without modifying that class:
```java
import org.apache.fory.json.ForyJson;
import org.apache.fory.json.annotation.JsonMixin;
import org.apache.fory.json.annotation.JsonProperty;
import org.apache.fory.json.annotation.JsonUnwrapped;
@JsonMixin(target = ThirdPartyUser.class)
abstract class ThirdPartyUserMixin {
@JsonProperty("user_id")
long id;
@JsonUnwrapped(prefix = "address_")
Address address;
}
ForyJson json = ForyJson.builder().registerMixin(ThirdPartyUserMixin.class).build();
```
A Mixin source is a named abstract class or interface, must not be local or anonymous, must not
extend or implement another type, and is never instantiated. Its annotated fields, methods,
constructors, and parameters select existing declarations on the exact target. The target continues
to own all Java types, values, access, and construction. A registration for a base class does not
affect a subclass, and an interface registration does not affect an implementation.
The source may apply any mapping annotation listed above. Declaring an annotation
on a matched source declaration replaces the target annotation of the same type as a whole; it does
not merge individual annotation members. `JsonType` cannot be added or removed by a Mixin.
Use `JsonMixinRemove` when the target's annotation should not be effective in this configuration:
```java
import org.apache.fory.json.annotation.JsonMixin;
import org.apache.fory.json.annotation.JsonMixinRemove;
import org.apache.fory.json.annotation.JsonRawValue;
@JsonMixin(target = ThirdPartyMessage.class)
abstract class QuotedMessageMixin {
@JsonMixinRemove(JsonRawValue.class)
String body;
}
```
The source selector must match exactly one target declaration even when it only removes an
annotation. Registering a different Mixin for the same target on one builder replaces the earlier
registration. Re-registering the same source is harmless. Each `build()` snapshots the current
last-registration-wins mapping, so later builder changes do not mutate an existing `ForyJson`. An
empty source is a no-op and clears an earlier source for the same target when registered later.
A `JsonCodec` supplied by a Mixin is the target's effective annotation. An exact
`registerCodec` registration still wins, while the effective type annotation wins over a built-in
mapping.
On Android and GraalVM Native Image, compile non-empty Mixins with the Fory annotation processor
so required generated operations and platform configuration are available. See the platform guides
linked above.
### `JsonProperty`
`JsonProperty` configures the canonical name, serialization index, and null inclusion of one
complete logical property. An annotation on a field, getter, or setter applies to the merged
field/getter/setter group.
```java
import org.apache.fory.json.annotation.JsonProperty;
public final class User {
@JsonProperty("user_id")
private long id;
@JsonProperty(include = JsonProperty.Include.ALWAYS)
private String displayName;
@JsonProperty(index = 10)
private String email;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
}
```
The supported inclusion values are:
- `DEFAULT`: use `ForyJsonBuilder.writeNullFields`.
- `ALWAYS`: write the property even when its selected value is null.
- `NON_NULL`: omit a null value.
Inclusion affects writing only. A non-default inclusion is invalid for a creator-only property with
no write source. Repeating the same declaration is allowed; conflicting explicit names, indexes, or
non-default inclusion policies within one logical property are rejected. Two properties that
normalize to the same final JSON name are also rejected.
`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.
`NON_EMPTY`, aliases, formatting, and independent read/write names are not supported.
`JsonProperty` cannot be combined with an Any logical property or declared on a `JsonAnySetter`.
### `JsonPropertyOrder`
`JsonPropertyOrder` combines a named serialization prefix, property indexes, and final-name
alphabetic ordering:
```java
import org.apache.fory.json.annotation.JsonProperty;
import org.apache.fory.json.annotation.JsonPropertyOrder;
@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`. The named prefix is written
first, remaining indexed properties follow in ascending index order, and `alphabetic = true` sorts
the remaining unindexed properties by final JSON name. Without `alphabetic`, those properties 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 is
locale-independent.
Order entries match the final JSON name first and the Java logical property name second. 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 object metadata is built.
A subclass declaration replaces both settings from its superclass as a whole. If the subclass has
no declaration, the nearest superclass declaration is used and resolved against the subclass
properties. Interface declarations are not considered. Ordering affects serialization only;
deserialization remains name-based, and subtype discriminators remain before user properties.
An unwrapped group also occupies one position, selected by the group's Java logical property name.
Its child members remain adjacent and retain the child's own order.
A write-enabled `JsonAnyProperty` or `JsonAnyGetter` participates as one position identified by its
Java logical property name. The position emits all dynamic entries in Map iteration order:
```java
import java.util.Map;
import org.apache.fory.json.annotation.JsonAnyProperty;
import org.apache.fory.json.annotation.JsonPropertyOrder;
@JsonPropertyOrder({"id", "properties", "timestamp"})
public final class Event {
public String id;
@JsonAnyProperty
public Map<String, Object> properties;
public long timestamp;
}
```
If `properties` contains `x` and `y`, output order is `id`, `x`, `y`, then `timestamp`; no member
named `properties` is written. Naming strategies do not transform the Any ordering name. An
input-only Any field and `JsonAnySetter` have no write position. Dynamic keys cannot be listed in
`JsonPropertyOrder`, and alphabetic ordering never sorts entries inside the Map.
### Property naming strategy
Configure the naming style for logical properties without an explicit non-empty `JsonProperty`
name:
```java
import org.apache.fory.json.PropertyNamingStrategy;
ForyJson json =
ForyJson.builder()
.withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.build();
```
The default `LOWER_CAMEL_CASE` preserves the discovered Java logical property name. `SNAKE_CASE`
handles acronym and digit boundaries, for example:
- `userName` becomes `user_name`;
- `URLValue` becomes `url_value`;
- `version2FA` becomes `version2_fa`.
A non-empty `@JsonProperty("...")` value, a parameter-local creator name, a subtype discriminator
property, and dynamic Any keys are already JSON names and are never transformed.
### `JsonIgnore`
`JsonIgnore` is field-targeted and controls the read and write directions of the complete logical
property:
```java
import org.apache.fory.json.annotation.JsonIgnore;
@JsonIgnore(ignoreRead = false, ignoreWrite = true)
private String serverManagedValue;
```
Both flags default to true. A same-named getter or setter cannot restore an ignored direction, and
`JsonProperty` cannot override it. Fory core's `Expose` annotation has no effect in Fory JSON.
### `JsonValue`
`JsonValue` selects one exact `String` field or public zero-argument method as the complete JSON
representation of its owning type. Fory writes the selected value as an ordinary JSON string, with
quotes and normal escaping, instead of writing the owning object's properties:
```java
import org.apache.fory.json.annotation.JsonCreator;
import org.apache.fory.json.annotation.JsonValue;
public final class UserId {
private final String value;
@JsonCreator
public UserId(String value) {
this.value = value;
}
@JsonValue
public String value() {
return value;
}
}
```
`json.toJson(new UserId("user-1"))` returns `"user-1"`. The method need not use a JavaBean getter
name. It must be public, non-static, zero-argument, and return exactly `String`; a field must be an
eligible non-static instance field. One type may have only one effective value member. An
unannotated method override suppresses an inherited declaration.
`JsonValue` controls serialization by itself. Deserialization additionally requires a
`JsonCreator` constructor or public static factory with exactly one `String` parameter, an empty
`JsonCreator.value()`, and no `JsonProperty` on that parameter. Fory recognizes that shape as the
reverse String constructor; no creator mode is needed. Existing property-list and parameter-local
creator forms are unchanged. Without the matching creator, writing still works and reading the
owning type fails clearly.
A null owner is written and read as JSON `null` without invoking either member or creator. A
non-null owner whose value member returns null is also written as JSON `null`. `JsonValue` does not
change Map key encoding.
### `JsonRawValue`
`JsonRawValue` marks one fixed ordinary `String` property. Fory writes the String directly at the
value position without quotes, escaping, parsing, or validation:
```java
import org.apache.fory.json.annotation.JsonRawValue;
public final class Response {
public int status;
@JsonRawValue
public String body;
}
```
With `status = 200` and `body = "{\"id\":1}"`, the output contains
`{"status":200,"body":{"id":1}}`. The raw String may be any complete JSON value, including an
object, array, number, boolean, quoted JSON string, or `null` token.
This annotation is a trusted write-only escape hatch. Invalid or attacker-controlled content can
make the enclosing output invalid or change its structure. Java null still follows the property's
normal inclusion policy and, when included, is written as JSON `null`.
Reading remains ordinary String-property reading. For example, `{"body":"text"}` can populate the
field, but an object such as `{"body":{"id":1}}` cannot be read back into it. `JsonRawValue` is not
a type-use annotation and does not apply to container elements or Map values. It cannot be placed
on a setter, creator parameter, Any declaration, or the same property occurrence as `JsonCodec`.
As an occurrence-local representation, it keeps the raw String shape even when the value type has
an exact builder-registered codec.
`JsonRawValue` does not collect unknown sibling fields. Unknown fields are skipped unless an
existing `JsonAnyProperty` or `JsonAnyGetter`/`JsonAnySetter` owner captures them. The raw-value and
Any-property features are independent.
`JsonValue` and `JsonRawValue` may be combined on the same String member to write an owning object
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`
`JsonBase64` selects a quoted standard Base64 JSON string for one exact `byte[]` field or getter:
```java
import org.apache.fory.json.annotation.JsonBase64;
public final class Attachment {
@JsonBase64
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.
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
`@JsonCodec(Base64ByteArrayCodec.class)`.
### `JsonUnwrapped`
Use `JsonUnwrapped` to place an object-valued property's members directly in the containing JSON
object:
```java
import org.apache.fory.json.annotation.JsonUnwrapped;
public final class Person {
public int age;
@JsonUnwrapped(prefix = "name_")
public Name name;
}
public final class Name {
public String first;
public String last;
}
```
This maps `Person` to `{"age":18,"name_first":"Ada","name_last":"Lovelace"}`. The
optional prefix and suffix apply to each child's final JSON name after `JsonProperty` and the
configured naming strategy. Nested unwrapped properties compose their transformations from the
inside out.
A null child writes no members. On input, Fory creates and assigns the child only after seeing one
of its flattened members. A completely missing group therefore preserves a mutable parent's
initializer value and leaves a record or creator argument at its normal missing-property default.
Partial input constructs the child with the ordinary defaults for its other properties.
Mutable classes, records, and `JsonCreator` classes can be parents or children. A parameter-local
creator parameter may declare a read-only unwrapped group; its required `JsonProperty` value names
the Java creator argument and is not accepted as a JSON wrapper. A parameterized parent is allowed,
but every unwrapped child and intermediate must be an exact raw, non-generic class using Fory's
standard object mapping.
The flattened group occupies one position in the parent's write order. `JsonProperty.index` may
position it, and `JsonPropertyOrder` selects it by Java logical property name. The child's own
property order remains intact. Parent fields are matched before flattened fields, which are matched
before dynamic Any handling.
Fory rejects duplicate or hash-colliding 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 valid on an unwrapped property; ordinary child leaf properties may still use
them.
### Dynamic object members
Use `JsonAnyProperty` when one `Map<String, V>` field should hold otherwise unknown JSON members.
The Map is flattened into the containing object instead of appearing under the field name:
```java
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.fory.json.annotation.JsonAnyProperty;
public final class Event {
public String id;
@JsonAnyProperty
public Map<String, Object> properties = new LinkedHashMap<>();
}
```
For `properties` containing `"source" -> "mobile"`, Fory writes
`{"id":"7","source":"mobile"}`, not a nested `properties` member. Unknown input members are
inserted into the Map. The field reads and writes by default; `JsonIgnore` may select one direction:
```java
import org.apache.fory.json.annotation.JsonIgnore;
@JsonAnyProperty
@JsonIgnore(ignoreRead = true, ignoreWrite = false)
public Map<String, Object> outputOnly;
```
During reading, an existing Map is reused. A null non-final field is initialized when the first
unknown member is encountered. 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. If no unknown member is present, Fory does not initialize
a null field.
Use `JsonAnyGetter` and `JsonAnySetter` for method-backed writing and reading:
```java
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.fory.json.annotation.JsonAnyGetter;
import org.apache.fory.json.annotation.JsonAnySetter;
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 is a public instance method with no arguments and a `Map<String, V>` return type. An
any-setter is a public instance method with signature `void method(String, V)`. Either method may be
used alone. When paired, their resolved value types must match after primitive types are boxed. A
primitive any-setter value parameter rejects JSON null. An any-setter is not supported on records or
types that use `JsonCreator`.
A read-enabled `JsonAnyProperty` on a record component supplies that component from unknown input
members. In property-list `JsonCreator` mode, a read-enabled Any field must correspond to one listed
creator argument; parameter-local creator mode cannot bind a field annotation. A write-only Any
field or any-getter cannot occupy a creator argument. If a write-only Any field or any-getter claims
a record component, that component receives its normal Java default during reading.
An any-getter claims its Java logical property: `getProperties()` and `properties()` both claim
`properties`. A same-named field, ordinary getter, or ordinary setter is not also mapped as a fixed
member. Fory does not infer a differently named backing field, so annotate that field with
`JsonIgnore` if it must not be mapped separately. `JsonAnySetter` has no logical property name and
does not claim a backing field.
The Any logical name is used only for property grouping and `JsonPropertyOrder`; it is not itself a
fixed JSON member. An input member with that name is an ordinary dynamic entry rather than a nested
aggregate, and the same dynamic output key remains valid unless another fixed property conflicts
with it.
One effective type hierarchy may use either one `JsonAnyProperty` field or at most one effective
`JsonAnyGetter` and one effective `JsonAnySetter`; the forms cannot be mixed. An unannotated method
override disables an inherited method annotation. Method-backed Any annotations are invalid in
field mode. `JsonProperty` is invalid on an Any setter and on every member of a logical property
claimed by an Any field or getter. A same-named field cannot use `JsonIgnore` to suppress an
any-getter's write direction. Its `ignoreRead` flag also does not disable a separate any-setter.
Dynamic keys are exact JSON member names and retain Map iteration order. A null Map writes no
members, and a null Map value writes 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. An output key is rejected when its Fory field-name hash conflicts with a
fixed property; this also covers 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 writes 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. Fixed input lookup is
also hash-based, so a differently spelled colliding name follows the fixed member instead of Any
handling. Repeated unknown input names replace the prior Map value, while an any-setter is invoked
for every occurrence. Escaped input names are decoded before delivery.
### `JsonCreator`
Use `JsonCreator` for an immutable class with one public constructor or public static factory. The
creator is the complete read schema; ordinary properties not selected by it are write-only, and
setters are not invoked after construction.
The compact form lists existing Java logical property names in parameter order and reuses their
normalized JSON metadata:
```java
import org.apache.fory.json.annotation.JsonCreator;
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;
}
}
```
The parameter-local form gives every parameter an explicit JSON name. It may introduce
creator-only input properties:
```java
@JsonCreator
public static User create(
@JsonProperty("user_id") long id,
@JsonProperty("display_name") String name) {
return new User(id, name);
}
```
Parameter-local names bypass the naming strategy. The two modes cannot be mixed. In compact mode,
names must be non-empty and unique, the name count must equal the parameter count, and parameters
must not also declare `JsonProperty`. In parameter-local mode, every parameter requires a
non-empty, unique `JsonProperty` name.
For a type with `JsonValue`, the empty form also accepts exactly one `String` parameter without
`JsonProperty` and reconstructs the owning value from its JSON string. This value form is distinct
from both property-based forms and is inferred only because the target has `JsonValue`.
A creator must have at least one parameter and cannot be varargs or generic. A constructor must be
public. A factory must be public and static, declare the target class as its exact return type, and
return a non-null value whose runtime class is exactly the target. Missing reference parameters use
null, missing primitives use Java zero values, duplicate members use the last value, and explicit
null for a primitive parameter is rejected. Records cannot declare a property-based `JsonCreator`;
a record with `JsonValue` may annotate its one-String canonical constructor for the value form.
### `JsonSubTypes`
`JsonSubTypes` declares the complete finite subtype table for an interface or abstract class. Each
entry has a case-sensitive logical JSON name and exactly one trusted Java type source:
- `value = Circle.class`; or
- `className = "com.example.shape.Circle"` using the exact Java binary name.
`className` is useful when an API JAR must not depend on an implementation JAR. It is resolved by
the fixed builder class loader when the table is built. JSON input never supplies a Java class name
and cannot add entries. Runtime registration and open subtype discovery are not supported.
The default `PROPERTY` inclusion writes an inline discriminator as the first output member:
```java
import org.apache.fory.json.annotation.JsonSubTypes;
@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 }
```
Property input accepts the discriminator at any direct object-member position, but it must appear
exactly once, be a string, and name a configured subtype. The discriminator property bypasses the
naming strategy and must not collide with a subtype's ordinary JSON property. Property inclusion
requires the subtype's ordinary object representation.
`WRAPPER_OBJECT` uses one outer member:
```java
@JsonSubTypes(
inclusion = JsonSubTypes.Inclusion.WRAPPER_OBJECT,
value = {@JsonSubTypes.Type(value = Circle.class, name = "circle")})
public interface Shape {}
```
```json
{ "circle": { "radius": 2 } }
```
`WRAPPER_ARRAY` uses exactly two array elements:
```java
@JsonSubTypes(
inclusion = JsonSubTypes.Inclusion.WRAPPER_ARRAY,
value = {@JsonSubTypes.Type(value = Circle.class, name = "circle")})
public interface Shape {}
```
```json
["circle", { "radius": 2 }]
```
The configuration rules are strict:
| Inclusion | `property` | Subtype representation |
| ---------------- | ---------------------- | ----------------------------------------------------- |
| `PROPERTY` | Required and non-empty | Ordinary object members inline with the discriminator |
| `WRAPPER_OBJECT` | Must be empty | Complete subtype value inside one-member object |
| `WRAPPER_ARRAY` | Must be empty | Complete subtype value as array element 1 |
Both wrappers may delegate to an exact custom subtype codec. All three inclusions write null as
plain JSON null unless codec precedence selects a custom complete-value codec for the declared
base, replacing the annotation.
The base must be an interface or abstract class. Every entry must resolve to a unique concrete,
assignable class, and serialization accepts only an exact listed runtime class. Listing a parent
does not implicitly admit its descendants. The annotation is read from the declared base itself and
is not inherited from another annotated interface or abstract class. Readers accept only the
configured inclusion; changing inclusion is a wire-format change and there is no dual-read
fallback.
At GraalVM native-image runtime, annotate the base with `JsonType` and use class-literal entries
rather than `className` entries. Listed class-literal subtypes are registered automatically.
## Custom codecs
`JsonValueCodec<T>` is Fory JSON's streaming codec SPI for one complete JSON value. It writes
directly to Fory's String or UTF-8 writer and reads directly from Fory's Latin-1, UTF-16, or UTF-8
reader. It is not a JSON abstract syntax tree (AST) or `JsonNode` codec. It owns the complete value,
including JSON null, but never handles a Map key; `MapKeyCodec` remains responsible for JSON object
member names.
For an application codec with the same semantics in every representation, extend
`AbstractJsonValueCodec<T>` and implement the JSON shape once:
```java
import java.math.BigDecimal;
import org.apache.fory.json.codec.AbstractJsonValueCodec;
import org.apache.fory.json.reader.JsonReader;
import org.apache.fory.json.writer.JsonWriter;
public final class MoneyCodec extends AbstractJsonValueCodec<Money> {
@Override
public void write(JsonWriter writer, Money value) {
if (value == null) {
writer.writeNull();
} else {
writer.writeBigDecimal(value.amount);
}
}
@Override
public Money read(JsonReader reader) {
return reader.tryReadNullToken() ? null : new Money(reader.readBigDecimal());
}
}
final class Money {
final BigDecimal amount;
Money(BigDecimal amount) {
this.amount = amount;
}
}
```
`AbstractJsonValueCodec` adds one virtual method call per operation. For a
performance-sensitive codec, or when behavior depends on a concrete reader or writer, implement
`JsonValueCodec<T>` directly and provide all five representation-specific methods.
```java
import org.apache.fory.json.ForyJson;
ForyJson json =
ForyJson.builder()
.registerCodec(Money.class, new MoneyCodec())
.build();
```
The containing property still controls its name, ignore direction, and null-inclusion policy. If a
null property is omitted, the value codec is not called. If the property is emitted, or the value
is an array element, collection element, map value, Optional value, or atomic-reference value, the
codec receives and owns null. The registered instance is shared across concurrent operations and
must be thread-safe.
Registering a custom codec for a `JsonSubTypes` base replaces that base's subtype annotation.
Registering one for a listed subtype is supported by the two wrapper inclusions but not by inline
property inclusion.
### Selecting codecs with `JsonCodec`
Use `@JsonCodec` on a class, record, enum, or interface to declare its default complete-value
codec. The positional form is shorthand for `value`:
```java
import org.apache.fory.json.annotation.JsonCodec;
@JsonCodec(MoneyCodec.class)
public final class Money {}
@JsonCodec(AccountCodec.class)
public interface Account {}
public final class RetailAccount implements Account {}
```
Type declarations are inherited through both superclasses and interfaces. The most-specific
declaration wins. Unrelated declarations using the same codec are consistent; unrelated
declarations using different codecs fail instead of depending on reflection order.
On a field or effective ordinary getter, `value` replaces the complete property value. The same
annotation is supported on an effective setter value parameter, a `JsonCreator` constructor or
factory parameter, and a record component through Java's field, accessor, and constructor-parameter
propagation:
```java
public final class Invoice {
@JsonCodec(MoneyCodec.class)
public Money total;
private Money tax;
private Money discount;
@JsonCodec(MoneyCodec.class)
public Money getTax() {
return tax;
}
public void setDiscount(@JsonCodec(MoneyCodec.class) Money discount) {
this.discount = discount;
}
@JsonCreator
public Invoice(@JsonProperty("total") @JsonCodec(MoneyCodec.class) Money total) {
this.total = total;
}
}
```
Use a child member when the standard container should remain in control and only its direct child
needs a custom codec:
```java
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
public final class InvoiceGroup {
@JsonCodec(elementCodec = MoneyCodec.class)
public List<Money> items;
@JsonCodec(elementCodec = MoneyCodec.class)
public Money[] itemArray;
@JsonCodec(elementCodec = MoneyCodec.class)
public AtomicReferenceArray<Money> atomicItems;
@JsonCodec(contentCodec = MoneyCodec.class)
public Optional<Money> optional;
@JsonCodec(contentCodec = MoneyCodec.class)
public AtomicReference<Money> current;
@JsonCodec(keyCodec = CurrencyKeyCodec.class, valueCodec = MoneyCodec.class)
public Map<Currency, Money> byCurrency;
}
```
The child members have these meanings:
| Member | Supported current value | Direct child handled by the codec |
| -------------- | ------------------------------------------------- | --------------------------------- |
| `elementCodec` | `Collection<E>`, `E[]`, `AtomicReferenceArray<E>` | `E` |
| `contentCodec` | `Optional<T>`, `AtomicReference<T>` | `T` |
| `keyCodec` | `Map<K, V>` | JSON member name for `K` |
| `valueCodec` | `Map<K, V>` | direct `V` value |
A custom Map-key codec converts between the declared key and a JSON member name:
```java
import java.util.Locale;
import org.apache.fory.json.codec.MapKeyCodec;
public final class CurrencyKeyCodec implements MapKeyCodec {
@Override
public String toName(Object key) {
return ((Currency) key).name().toLowerCase(Locale.ROOT);
}
@Override
public Object fromName(String name) {
return Currency.valueOf(name.toUpperCase(Locale.ROOT));
}
}
```
Code that used the removed type-use form should move the codec to the owning declaration:
```java
// Before
List<@JsonCodec(MoneyCodec.class) Money> items;
// Now
@JsonCodec(elementCodec = MoneyCodec.class)
List<Money> items;
```
Use `contentCodec` for an `Optional` or `AtomicReference`, `valueCodec` for a Map value, and
`elementCodec` for an array or `AtomicReferenceArray` element.
`Iterable<E>` values that are not `Collection<E>` do not support `elementCodec`. Use `value` when a
complete codec should own such a value.
Child configuration is intentionally one level deep. For `List<List<Money>>`, `elementCodec`
handles each complete `List<Money>`. For `Money[][]`, it handles each `Money[]`. To customize a
deeper descendant, implement a codec for the complete current value and select it with `value`.
`value` is mutually exclusive with every child member because it already owns the complete current
value. An empty annotation, an unsupported child member, or an outer complete codec combined with
a child member fails during model construction. A configured direct child must resolve to a
concrete type; raw containers, direct wildcards, and unresolved direct type variables are rejected.
`JsonAnyProperty` and `JsonAnyGetter` flatten their Map into the enclosing object. Configure their
dynamic values with `valueCodec`:
```java
@JsonAnyProperty
@JsonCodec(valueCodec = MoneyCodec.class)
public Map<String, Money> extra;
```
The first `JsonAnySetter` parameter is the String property name. Its second parameter may use
`@JsonCodec(value = ...)` or another configuration valid for that parameter's own shape.
### Codec precedence and repeated declarations
Fory resolves each current value in this order:
| Priority | Source |
| -------: | ------------------------------------------------- |
| 1 | Current property or parameter `JsonCodec` |
| 2 | Exact `registerCodec` registration |
| 3 | Exact-target type `JsonCodec` after Mixin overlay |
| 4 | Inherited-frontier type `JsonCodec` declaration |
| 5 | Built-in or default JSON mapping |
One logical property may expose the annotation from its field, getter, setter parameter, creator
parameter, or record propagation. Repeated configurations must be identical; Fory does not merge
partial configurations from different declarations. An unannotated effective override suppresses
an inherited method annotation.
A child member replaces only that direct child. Unconfigured Map siblings continue through the
normal precedence. If an exact registration or type declaration supplies a complete codec for the
outer container, a property child member is unreachable and therefore rejected.
Map keys are JSON object member names and use `MapKeyCodec`, not `JsonValueCodec`. A custom key
codec class follows the same construction rules as a value codec. Null Map keys are rejected, and
decoded keys must match the declared key type.
### Codec construction and platform support
An annotation codec class must be public, concrete, top-level or static nested, and have a public
no-argument constructor. One instance is shared by all annotated sites and concurrent operations of
the built `ForyJson`, so it must be thread-safe. Use `registerCodec(Target.class, instance)` when a
complete-value codec needs configuration.
In a named Java module, export or open the codec package to `org.apache.fory.json`. When an inherited
type-declaration codec is used for a more specific target, every decoded value must be null or
assignable to that target.
The annotation has the same FIELD, METHOD, and PARAMETER behavior on the JVM, Android, and GraalVM
Native Image. Ordinary Android classes may omit `JsonType` and provide equivalent exact rules.
Android-desugared Records, including `JsonValue` Records, require processor-generated operations
from either a direct `JsonType` declaration or a compiled exact `JsonMixin` pair. GraalVM object
models follow the build-time workflow in the
[GraalVM guide](../../docs/guide/java/graalvm-support.md).
## Type validation and untrusted input
Fory JSON never derives a Java class name from JSON input. It always applies Fory's fixed disallow
list. Add an application allow-list with `withTypeChecker` when only selected model packages should
be mapped:
```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 is used while application types are prepared for both serialization and parsing and must be
thread-safe. Built-in scalar types normally do not invoke the custom checker, but selecting an
application codec for a built-in target makes that target subject to the checker. A custom codec
does not bypass Fory's fixed disallow list.
`withClassLoader` sets the fixed loader for annotation-declared subtype `className` entries. If it
is not configured, `build()` snapshots the current thread context class loader and falls back to the
loader that defined `ForyJson`. Later thread context loader changes do not affect the runtime.
`maxDepth` limits nested arrays and objects but is not an input-byte or memory quota. Apply external
request size, timeout, and resource controls appropriate to the application's trust boundary.
The following types are rejected by default because their natural JSON mapping would create unsafe
or ambiguous behavior: `Class`, `URL`, `InetAddress`, and `InetSocketAddress`. `URL` may be supported
with an application-owned exact custom codec. Arbitrary `Number` and `CharSequence` subclasses also
require an exact supported or custom codec.
## Limits and unsupported features
Fory JSON intentionally has a smaller semantic surface than the Fory binary protocol and general
Jackson object mapping:
- no shared-reference identity or circular-reference protocol;
- no open polymorphism, JSON class-name IDs, runtime subtype discovery, or runtime subtype table
extension;
- 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 Fory core `Expose` processing.
Circular graphs eventually fail `maxDepth`; they are not reconstructed. Use Fory core's binary
native or xlang protocol when reference identity or cycles are required.
## Errors and troubleshooting
| Symptom | Likely cause and action |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ForyJsonException` while parsing | Invalid JSON grammar, type mismatch, unsupported mapping, depth violation, or trailing content; inspect the message and target type |
| `InsecureException` | Fory's disallow list or the configured `JsonTypeChecker` rejected a class |
| `IllegalArgumentException` from a builder | Check the configured depth, concurrency, retained-buffer, and cached-field-name limits |
| Declared write is rejected | The value is not assignable to the declared type, the type contains a wildcard/type variable, or null was supplied for a primitive |
| Immutable value is not populated | Use a record, a valid `JsonCreator`, or an exact custom codec |
| `JsonValue` read fails | Add one plain `String` `JsonCreator`, or register an exact custom codec |
| Raw JSON output is invalid | Supply exactly one trusted, complete JSON value to the `JsonRawValue` property |
| Ordinary object cannot be constructed | Add a usable no-argument constructor, use a record or `JsonCreator`, or register a custom codec; Android and GraalVM native image are stricter |
| Ordinary accessor annotation fails | The method is not an eligible public JavaBean accessor, or field mode is enabled |
| Any annotation fails | Use exactly one field-backed form or one valid method-backed pair with resolved `Map<String, V>` types; method annotations require non-field mode |
| Codec annotation fails | Resolve same-node or hierarchy conflicts, remove a hidden nested override, or use a public no-argument codec class |
| Subtype is rejected | The base is not declared on the write, the runtime class is not an exact table entry, or the input wire shape differs from the configured inclusion |
| Collection cannot be read | Target a supported interface/common implementation or register a custom codec |
| OutputStream write fails | The underlying `IOException` is wrapped as the cause of `ForyJsonException` |
Fory JSON mapping, syntax, codec, depth, and output failures use `ForyJsonException`. User code may
still throw its own runtime exception. Creator exceptions other than `Error` are wrapped with their
original cause.
## Related documentation
- [Fory JSON website guide](https://fory.apache.org/docs/guide/java/json_support)
- [Java native serialization](https://fory.apache.org/docs/guide/java/native_serialization)
- [Java xlang serialization](https://fory.apache.org/docs/guide/java/xlang_serialization)
- [Java configuration](https://fory.apache.org/docs/guide/java/configuration)