Skip to main content
Version: 1.7

Object Mapping

Thread safety, reuse, and code generation​

ForyJson is immutable and thread-safe after build(). Reuse one instance instead of creating a builder and ForyJson instance 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 sets the maximum number of root operations that execute concurrently. Additional callers wait until one of those fixed execution states is available. Root APIs on one ForyJson instance are not reentrant: a custom codec must continue through the concrete reader or writer passed to it instead of calling toJson, toJsonBytes, writeJsonTo, or fromJson on that instance.

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.

Eligible instance fields, including ordinary final fields, are restored when reading JSON. When a subclass hides a superclass field with the same name, the subclass field is the JSON property in both directions. Ignoring that field does not expose the hidden superclass field.

An ordinary object property uses its declared type when reading. When writing a subclass instance through a concrete declared base type, Fory writes the subclass's mapped properties. Reading the same JSON through the base type creates a base instance and ignores subclass-only properties. Use @JsonSubTypes or a custom codec when the reader must restore the subtype.

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.

Required constructor properties​

Enable failOnMissingRequiredProperties(true) to reject missing ordinary constructor or factory properties that have no declared default:

ForyJson json = ForyJson.builder()
.failOnMissingRequiredProperties(true)
.build();

The option defaults to false and applies to records, property-based JsonCreator models, Scala case classes, and Kotlin constructor models, including nested objects. Declared language defaults and existing optional, collection, map, and array defaults remain available. Ordinary properties without declared defaults must appear instead of receiving zero, false, or null. No new container defaults are introduced: a Kotlin non-null collection without a default remains required, while a Scala collection retains its empty default.

With the default onNullRead(SET), an explicit JSON null counts as present and follows the property's type and nullability rules. With SKIP, that occurrence is omitted and cannot satisfy a required parameter. Ignored properties, ordinary no-argument beans, and properties assigned after construction keep their existing behavior. Complete custom object codecs own their own missing-field rules.

This option does not change writing. If an inclusion policy omits a required property, the resulting JSON can be rejected by a strict reader. Preserve that property in the output or use a reader with the option disabled.

Kotlin object mapping​

Install fory-json-kotlin and use ForyJsonKotlin.builder() for Kotlin/JVM classes. Kotlin ordinary and data classes use their selected constructor, exact property types, compiler defaults, and declared nullability; they do not use Java's constructor-bypassing fallback. A default applies when the member is missing or its null occurrence is skipped. By default, explicit JSON null is a present value and is rejected for a non-null parameter.

Use jsonTypeRef<T>() for generic, nullable, unsigned, and value-class roots. Standard arrays, collections, and maps continue to use their normal Fory JSON representation. The complete language type table, singleton/value-class behavior, and omission rules are in the Kotlin guide.

Read-side null handling​

Use independent builder settings to retain existing property values and omit null container items:

import org.apache.fory.json.annotation.JsonProperty.NullHandling;

ForyJson json = ForyJson.builder()
.onNullRead(NullHandling.SKIP)
.onContentNullRead(NullHandling.SKIP)
.build();

Both settings default to SET. FAIL rejects an explicit null; SKIP omits that input occurrence. The builder accepts concrete modes only, not DEFAULT or Java null. Each built instance retains its own configuration. Use @JsonProperty(onNullRead = SET) or onContentNullRead = SET to opt a property out of the matching global rule. Content settings cover immediate collection/array elements and map values, including root containers and dynamic JSON trees. Property settings apply to typed object properties, including nested objects, and do not alter a root null value. See annotation semantics and examples.

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

Declared boolean and numeric targets also accept their ordinary token text in a JSON string, such as "true", "42", or "123.45"; no builder option is required. This applies to roots, object members, arrays, collections, and maps. Fory continues to write native JSON boolean and number tokens. An Object target retains natural JSON typing, so a quoted value remains a String; a quoted numeric value read as Number uses Double.

Built-in representations​

These built-in values use the following ordinary JSON shapes:

Java typeJSON representation
Boolean and numeric scalarsNative JSON boolean or number when written; declared targets also read the same token text quoted
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​

To omit empty object properties by default:

import org.apache.fory.json.ForyJson;
import org.apache.fory.json.annotation.JsonProperty.Include;

ForyJson json = ForyJson.builder().defaultPropertyInclusion(Include.NON_EMPTY).build();

defaultPropertyInclusion and writeNullFields update the same setting; the last call wins. The builder accepts ALWAYS, NON_NULL, and NON_EMPTY. It rejects DEFAULT and NON_DEFAULT. Default omission requires explicit @JsonProperty(include = NON_DEFAULT) or class-level @JsonInclude(NON_DEFAULT) authorization; see Default omission for supported sources, construction/evaluation effects, and errors. Both forms retain properties without defaults, including null and zero values. An explicit JsonProperty.include overrides JsonInclude on the class, which overrides the builder default. See Property inclusion for the empty-value definitions and boundaries.

Builder methodDefaultUser-visible effect
defaultPropertyInclusion(Include)NON_NULLDefault inclusion of object properties
writeNullFields(boolean)falseSelect ALWAYS when true or NON_NULL when false
writeLongAsString(boolean)falseWrite built-in 64-bit integer values as decimal strings
escapeNonAscii(boolean)falseEscape non-ASCII characters in generated JSON strings and names
byteArrayFormat(JsonByteArray.Format)BASE64Default byte-array representation for reading and writing
withCodegen(boolean)trueEnable generated object codecs
withAsyncCompilation(boolean)trueCompile generated codecs asynchronously
onNullRead(NullHandling)SETHandle explicit null property values
onContentNullRead(NullHandling)SETHandle null array/collection elements and map values
withFieldMode(boolean)falseWhen true, discover fields without getters/setters
withPropertyNamingStrategy(strategy)LOWER_CAMEL_CASEName properties without an explicit JsonProperty name
withMaxCachedFieldNames(int)DEFAULT_MAX_CACHED_FIELD_NAMES (8192)Field-name cache entries per reader; zero disables caching
withConcurrencyLevel(int)max(1, 2 * processors)Maximum concurrent root operations
withBufferSizeLimitBytes(int)2 MiBMaximum retained output/string-decoding buffer capacity
registerCodec(type, codec)NoneReplace an eligible exact class's complete JSON codec
registerMixin(mixinType)NoneApply one annotation Mixin to its exact declared target

Use byteArrayFormat to choose one representation for ordinary byte[] roots, unannotated properties, and container values, including nested arrays, collections, maps, and optionals:

import org.apache.fory.json.ForyJson;
import org.apache.fory.json.annotation.JsonByteArray;

ForyJson hexJson = ForyJson.builder().byteArrayFormat(JsonByteArray.Format.BASE16).build();
String text = hexJson.toJson(new byte[] {1, -2, 3}); // "\"01fe03\""
byte[] bytes = hexJson.fromJson(text, byte[].class);

BASE64 is the default and writes padded standard Base64 strings. BASE16 writes lowercase hexadecimal strings without prefixes or separators; reading accepts either case and JSON string escapes. Odd-length strings and non-hexadecimal characters are rejected. ARRAY uses signed numbers in [-128, 127]. Empty arrays become "" for either string format or [] for ARRAY. All three formats accept JSON null. The reader uses the configured representation and does not guess another format. A property's JsonByteArray annotation, including one supplied by a Mixin, overrides the global default. Custom occurrence codecs retain their complete representation; Kotlin unsigned semantic arrays remain numeric arrays.

Enable writeLongAsString(true) when 64-bit integer values must pass through JavaScript without Number precision loss. The setting writes built-in long/Long, AtomicLong, AtomicLongArray, and OptionalLong values as quoted decimal strings. It also follows declared Long children through arrays, collections, map values, Optional<Long>, AtomicReference<Long>, and equivalent language-module containers. Readers accept both numeric and quoted integer tokens regardless of this setting. Custom codecs and occurrence-level codec or format annotations retain their own output shape.

Concurrency-level and buffer-retention limits must be positive. The cached-field-name limit applies independently to each reader; zero disables this cache. It bounds only cached field names, not names accepted from the input. The buffer-retention setting does not limit JSON input or output size. It limits the capacity of each reusable output or string-decoding buffer after an operation, not the total memory used by the instance.

For class loading, type policy, nesting depth, graph-memory limits, and external input controls, see Fory JSON Security.

Builder mutation after build() does not modify an existing ForyJson instance.

On Android, runtime code generation and asynchronous compilation are disabled. In a GraalVM native image, runtime compilation is unavailable. Fory JSON generates codecs for reachable models with the default configuration and each reachable ForyJsonProvider configuration. A model without a matching generated codec uses an interpreted codec. Every other builder option keeps the behavior described above.

Non-ASCII escaping​

Enable escaping when the receiver requires non-ASCII characters to be represented with JSON Unicode escapes:

import org.apache.fory.json.ForyJson;

ForyJson json = ForyJson.builder().escapeNonAscii(true).build();
String text = json.toJson("汉é😀"); // "\u6c49\u00e9\ud83d\ude00"
byte[] utf8 = json.toJsonBytes("汉é😀");

The option applies to string and character values, property names, map keys, enum names, and subtype names through all String, UTF-8, stream, and pretty write APIs. Characters above U+007F use lowercase hexadecimal escapes; supplementary characters use two UTF-16 surrogate escapes. JSON-required escaping is unchanged, and invalid surrogate sequences in strings still fail. ASCII characters such as <, >, and & are not additionally escaped.

The setting defaults to false and is fixed for each built instance. Reuse two instances when alternating output policies. Pretty output remains a per-call choice, and reading is unaffected. Disabling the option preserves the existing output representation, including any escapes it already uses.

Caller-supplied raw JSON, including @JsonRawValue, is preserved verbatim. A document containing raw JSON can therefore still contain non-ASCII characters. Custom codecs should use structured string-writing methods to honor the option; raw writes remain the codec author's responsibility.

Pretty printing​

Choose readable output for an individual serialization call:

ForyJson json = ForyJson.builder().build();
String text = json.toPrettyJson(value);
byte[] utf8 = json.toPrettyJsonBytes(value);
String compact = json.toJson(value);

The format matches Jackson's pretty printer configured with two-space indenters for both objects and arrays. Each container adds one indentation level, and colons have a space on each side. Empty objects and arrays are { } and [ ]. Line breaks use \n, with no trailing line break. Both APIs preserve logical string values and honor escapeNonAscii. Raw JSON values remain verbatim, including their supplied whitespace. Reading accepts both compact and pretty JSON. Pretty output is selected per call; the same instance can alternate formats. Existing toJson, toJsonBytes, and writeJsonTo calls continue to produce compact output.