Skip to main content
Version: dev

Annotations

Fory JSON provides these mapping and validation annotations in org.apache.fory.json.annotation: JsonAnyGetter, JsonAnyProperty, JsonAnySetter, JsonBase64, JsonCodec, JsonCreator, JsonFormat, JsonIgnore, JsonProperty, JsonPropertyOrder, JsonRawValue, JsonSubTypes, JsonUnwrapped, JsonValidator, 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 on the JVM and Android. It is not inherited, so annotate each eligible concrete model that needs a generated companion on those runtimes. 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. Outside Native Image, a directly annotated model that uses the default object codec fails during codec creation if its generated companion is missing. GraalVM Native Image discovers JsonType directly and does not use annotation-processor output. See the GraalVM guide and Android guide for the platform workflows.

Mixins

Use a JSON Mixin to apply Fory JSON mapping and validation 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 or validation 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, compile non-empty Mixins with the Fory annotation processor so required generated operations and platform configuration are available. GraalVM Native Image discovers reachable Mixins directly. 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, JsonFormat, or an Any declaration. The equivalent explicit codec is @JsonCodec(Base64ByteArrayCodec.class).

JsonFormat

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

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

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

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

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

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

@JsonFormat(pattern = "uuuu-MM-dd HH:mm:ss XXX", timezone = "Asia/Shanghai")
public Instant timestamp;
}

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

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

Set timezone to a valid ZoneId identifier to format and parse Instant, ZonedDateTime, or OffsetDateTime in that zone. For example, the timestamp field above writes Instant.parse("2024-01-02T03:04:05Z") as "2024-01-02 11:04:05 +08:00". The parsed value keeps the same instant for matching timezone text. The configured zone supplies missing zone or offset information during parsing; an explicit zone or offset in the JSON text participates in the usual DateTimeFormatter resolution. Include an offset in the pattern when an exact instant must survive a daylight saving time overlap. Omitting timezone preserves the default behavior described above. Invalid zone identifiers and a non-empty timezone on other supported date/time types are rejected.

JsonFormat is a field annotation, not a type-use annotation. A record component works through its generated field. Nested wrappers, Map keys, raw or wildcard direct children, JSON Any values, and unwrapped values are intentionally rejected. Types with ambiguous formatting semantics, including legacy and SQL date types, Duration, Period, TimeZone, ZoneId, and ZoneOffset, are not supported. A wrapper with a complete registered, annotation-selected, polymorphic, or JsonValue representation is also rejected because that representation owns the whole wrapper. JsonFormat cannot share a field with JsonCodec, JsonBase64, JsonRawValue, JsonAnyProperty, JsonUnwrapped, or JsonValue.

JsonUnwrapped

Use JsonUnwrapped to place an object-valued property's members directly in the containing JSON 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 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, JsonCodec, and JsonFormat 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 that conflicts with a fixed property is rejected. Fory does not inspect an Any Map for a key that duplicates an inline subtype discriminator; such a key writes a duplicate JSON member. Applications must keep dynamic keys distinct from the active discriminator. 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.

JsonValidator

Use JsonValidator for application validation that must run after an object has been completely constructed and populated:

import org.apache.fory.json.annotation.JsonValidator;

public final class Account {
public String id;
public long balance;

@JsonValidator
public void validate() {
if (id == null || id.isEmpty()) {
throw new IllegalArgumentException("id must not be empty");
}
if (balance < 0) {
throw new IllegalArgumentException("balance must not be negative");
}
}
}

A validator must be a public instance method with no arguments and a void return type. The method may declare exceptions. Every effective validator runs exactly once after its object is complete, including objects created by a JsonCreator, records, nested objects, unwrapped objects, and selected subtypes. A JSON null value does not invoke a validator. If a class has multiple validators, their relative order is unspecified and validation stops at the first failure. JsonValidator has no index or ordering member.

An invalid validator declaration is rejected when Fory JSON prepares the type. Error is propagated directly; every other validator invocation failure is reported as ForyJsonException with the original cause. A JsonCreator constructor or factory may validate during construction instead; omit JsonValidator when the creator already enforces the complete invariant.

A Mixin can add validation to a matching public method on an exact target:

import org.apache.fory.json.annotation.JsonMixin;
import org.apache.fory.json.annotation.JsonValidator;

@JsonMixin(target = ThirdPartyAccount.class)
abstract class ThirdPartyAccountMixin {
@JsonValidator
public abstract void checkValid();
}

The Mixin method uses the same exact method-signature matching as other Mixin methods. Remove a target validator for one configuration by placing @JsonMixinRemove(JsonValidator.class) on the matching Mixin method. An unannotated override is the effective declaration and does not inherit the overridden method's validator annotation.

JsonValidator applies to Fory JSON's default object mapping. An exact registered codec, a complete type-level JsonCodec, or a complete JsonValue representation must perform any required validation itself.

On Android, compile a directly annotated validator model with JsonType and the Fory annotation processor. A validator supplied by a Mixin uses the processor output for that exact Mixin-target pair. GraalVM Native Image discovers a direct JsonType or registered Mixin and prepares its effective validators without annotation-processor output. Neither platform requires application reflection configuration for validators.

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.