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.

RepresentationOperationfory-json ops/secJackson ops/secGson ops/sec
StringSerialize7,387,4652,049,3681,084,042
StringDeserialize2,897,9551,074,885902,772
UTF-8 bytesSerialize10,375,4981,868,6141,037,211
UTF-8 bytesDeserialize3,077,1581,268,397933,079

See the full benchmark report.

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:

<dependency>
  <groupId>org.apache.fory</groupId>
  <artifactId>fory-json</artifactId>
  <version>1.4.0</version>
</dependency>

Gradle:

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:

--add-opens=java.base/java.lang.invoke=ALL-UNNAMED

For a module-path application:

--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.

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.

OperationRuntime typeDeclared ClassDeclared TypeRef
String outputtoJson(value)toJson(value, type)toJson(value, typeRef)
UTF-8 bytestoJsonBytes(value)toJsonBytes(value, type)toJsonBytes(value, typeRef)
UTF-8 OutputStreamwriteJsonTo(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:

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:

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:

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:

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:

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.

GroupSupported types and behavior
Core scalarsboolean, numeric primitives, char, their boxed types, String, CharSequence, StringBuilder, StringBuffer
NumbersNumber, BigInteger, BigDecimal, Fory Float16 and BFloat16, AtomicInteger, AtomicLong
EnumsEnum constant names as JSON strings
ArraysPrimitive arrays, boxed arrays, String arrays, object arrays, and multidimensional arrays
CollectionsCollection, List, Set, Queue, deque, blocking, sorted, and navigable interfaces; their abstract bases; EnumSet; and concrete implementations with an accessible no-argument constructor
MapsMap, sorted, navigable, and concurrent interfaces; AbstractMap; EnumMap; and concrete implementations with an accessible no-argument constructor
Optional and atomicOptional, OptionalInt, OptionalLong, OptionalDouble, AtomicBoolean, AtomicReference, and atomic arrays
TimeDate, Calendar, TimeZone, LocalDate, LocalTime, LocalDateTime, Instant, Duration, ZoneOffset, ZoneId, ZonedDateTime, Year, YearMonth, MonthDay, Period, OffsetTime, OffsetDateTime, and supported chronology dates
Other JDK typesUUID, URI, File, Path, Locale, Charset, Currency, Pattern, BitSet, ByteBuffer
Optional modulesjava.sql.Date, Time, and Timestamp; Guava ImmutableList, ImmutableSet, ImmutableSortedSet, ImmutableMap, ImmutableBiMap, ImmutableSortedMap, and ImmutableIntArray when Guava is present
ObjectsMutable 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 typeJSON representation
EnumConstant name as a string
Date, Calendar, java.sql.Date, Time, TimestampEpoch milliseconds as a number
TimeZoneTime-zone ID as a string
Java time and supported chronology date typesTheir standard textual form as a string
UUID, URI, File, Path, Locale, Charset, Currency, PatternType-specific text as a string; File and Path use path text, Locale uses a language tag, and Pattern does not retain flags
BitSetArray of signed long words from BitSet.toLongArray()
ByteBufferArray of signed byte values for the remaining range from position to limit
Optional and atomic wrappersTheir 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 valueJava value
ObjectJsonObject
ArrayJsonArray
StringString
BooleanBoolean
Integer within long rangeLong
Larger integerBigInteger
Fraction or exponentDouble
Nullnull

JsonObject preserves member insertion order and JsonArray is mutable. They can also be created and written directly.

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 methodDefaultUser-visible effect
writeNullFields(boolean)falseDefault inclusion of null object properties
withCodegen(boolean)trueEnable generated object codecs
withAsyncCompilation(boolean)trueCompile generated codecs asynchronously
withFieldMode(boolean)falseWhen true, discover fields without getters/setters
withPropertyNamingStrategy(strategy)LOWER_CAMEL_CASEName properties without an explicit JsonProperty name
withClassLoader(loader)Snapshotted thread context loader, then Fory JSON loaderResolve annotation-declared subtype class names
maxDepth(int)20Maximum 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 MiBMaximum reusable capacity retained by each pooled writer
registerCodec(type, codec)NoneReplace the exact class's complete JSON codec
registerMixin(mixinType)NoneApply one annotation Mixin to its exact declared target
withTypeChecker(checker)No custom checkerApply 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 and Android guide for the platform workflows.

Mixins

Use a JSON Mixin to apply Fory JSON mapping annotations to a class without modifying that class:

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:

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.

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

@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:

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 {}
{ "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:

@JsonSubTypes(
    inclusion = JsonSubTypes.Inclusion.WRAPPER_OBJECT,
    value = {@JsonSubTypes.Type(value = Circle.class, name = "circle")})
public interface Shape {}
{ "circle": { "radius": 2 } }

WRAPPER_ARRAY uses exactly two array elements:

@JsonSubTypes(
    inclusion = JsonSubTypes.Inclusion.WRAPPER_ARRAY,
    value = {@JsonSubTypes.Type(value = Circle.class, name = "circle")})
public interface Shape {}
["circle", { "radius": 2 }]

The configuration rules are strict:

InclusionpropertySubtype representation
PROPERTYRequired and non-emptyOrdinary object members inline with the discriminator
WRAPPER_OBJECTMust be emptyComplete subtype value inside one-member object
WRAPPER_ARRAYMust be emptyComplete 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:

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.

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:

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:

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:

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:

MemberSupported current valueDirect child handled by the codec
elementCodecCollection<E>, E[], AtomicReferenceArray<E>E
contentCodecOptional<T>, AtomicReference<T>T
keyCodecMap<K, V>JSON member name for K
valueCodecMap<K, V>direct V value

A custom Map-key codec converts between the declared key and a JSON member name:

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:

// 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:

@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:

PrioritySource
1Current property or parameter JsonCodec
2Exact registerCodec registration
3Exact-target type JsonCodec after Mixin overlay
4Inherited-frontier type JsonCodec declaration
5Built-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.

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:

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

SymptomLikely cause and action
ForyJsonException while parsingInvalid JSON grammar, type mismatch, unsupported mapping, depth violation, or trailing content; inspect the message and target type
InsecureExceptionFory's disallow list or the configured JsonTypeChecker rejected a class
IllegalArgumentException from a builderCheck the configured depth, concurrency, retained-buffer, and cached-field-name limits
Declared write is rejectedThe 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 populatedUse a record, a valid JsonCreator, or an exact custom codec
JsonValue read failsAdd one plain String JsonCreator, or register an exact custom codec
Raw JSON output is invalidSupply exactly one trusted, complete JSON value to the JsonRawValue property
Ordinary object cannot be constructedAdd 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 failsThe method is not an eligible public JavaBean accessor, or field mode is enabled
Any annotation failsUse 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 failsResolve same-node or hierarchy conflicts, remove a hidden nested override, or use a public no-argument codec class
Subtype is rejectedThe 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 readTarget a supported interface/common implementation or register a custom codec
OutputStream write failsThe 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