| --- |
| title: Annotations |
| sidebar_position: 4 |
| id: annotations |
| license: | |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| --- |
| |
| Fory JSON 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 model marker. They are |
| Fory JSON APIs, not Jackson, Gson, or Fory binary-protocol compatibility annotations. |
| |
| `JsonType` is not inherited, so mark each eligible concrete model that must participate in a |
| platform build workflow. For Java source, the Fory annotation processor generates direct property |
| and creator operations plus exact retention rules on the JVM and Android. A directly annotated |
| `JsonValue` Record also receives generated value-access and canonical-constructor operations. |
| Ordinary unannotated Java 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 Java model that uses the default object codec fails |
| during codec creation if its generated Java operations are missing. |
| |
| Kotlin/JVM models are mapped from validated Kotlin metadata and do not require generated |
| construction operations. In Android builds that use R8 or ProGuard, Kotlin KSP emits exact |
| retention rules for Kotlin `@JsonType` models. It also processes an exact Mixin declared in |
| application source when either the Mixin or its exact target is Kotlin. KSP does not generate codecs |
| or construction operations. GraalVM Native Image discovers reachable Java and Kotlin `JsonType` |
| declarations directly, and provider-selected configurations generate codecs while the image is |
| built. See the [GraalVM guide](graalvm.md) and [Android guide](android.md) for the platform workflows. |
| |
| ## Kotlin use-site targets |
| |
| Kotlin annotations merge into the same logical property as their Java field, accessor, or selected |
| constructor parameter. Use explicit targets so behavior does not depend on Kotlin's default-target |
| policy: |
| |
| | Kotlin site | Logical declaration | |
| | ------------ | ---------------------------------------------------------- | |
| | `@field:` | backing field | |
| | `@get:` | getter | |
| | `@set:` | setter | |
| | `@param:` | selected constructor parameter | |
| | `@setparam:` | setter value parameter for supported parameter annotations | |
| |
| `@property:` is unsupported because Fory JSON annotations do not target Kotlin-only property |
| metadata. `@setparam:JsonProperty` is rejected because setter-parameter naming is not a JSON |
| property-name contract. `@setparam:JsonIgnore`, `@setparam:JsonCodec`, and |
| `@setparam:JsonUnwrapped` apply to the exact one-argument setter property. An effective |
| `@set:JsonCodec` is also supported directly. |
| |
| `JsonProperty` members merge individually when their explicit values agree; conflicting names, |
| indexes, or inclusion policies fail. `JsonIgnore` read/write directions merge monotonically, and |
| repeated `JsonCodec` declarations must be identical. Mixin replacement or removal happens before |
| this merge. See [Kotlin](kotlin.md#annotations-and-use-site-targets) for an idiomatic example. |
| |
| ## Mixins |
| |
| Use a JSON Mixin to apply Fory JSON mapping and validation annotations to a class without modifying |
| that class: |
| |
| ```java |
| import org.apache.fory.json.ForyJson; |
| import org.apache.fory.json.annotation.JsonMixin; |
| import org.apache.fory.json.annotation.JsonProperty; |
| import org.apache.fory.json.annotation.JsonUnwrapped; |
| |
| @JsonMixin(target = ThirdPartyUser.class) |
| abstract class ThirdPartyUserMixin { |
| @JsonProperty("user_id") |
| long id; |
| |
| @JsonUnwrapped(prefix = "address_") |
| Address address; |
| } |
| |
| ForyJson json = ForyJson.builder().registerMixin(ThirdPartyUserMixin.class).build(); |
| ``` |
| |
| A Mixin source is a named abstract class or interface, must not be local or anonymous, must not |
| extend or implement another type, and is never instantiated. Its annotated fields, methods, |
| constructors, and parameters select existing declarations on the exact target. The target continues |
| to own all Java types, values, access, and construction. A registration for a base class does not |
| affect a subclass, and an interface registration does not affect an implementation. |
| |
| The source may apply any mapping 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: |
| |
| ```java |
| import org.apache.fory.json.annotation.JsonMixin; |
| import org.apache.fory.json.annotation.JsonMixinRemove; |
| import org.apache.fory.json.annotation.JsonRawValue; |
| |
| @JsonMixin(target = ThirdPartyMessage.class) |
| abstract class QuotedMessageMixin { |
| @JsonMixinRemove(JsonRawValue.class) |
| String body; |
| } |
| ``` |
| |
| The source selector must match exactly one target declaration even when it only removes an |
| annotation. Registering a different Mixin for the same target on one builder replaces the earlier |
| registration. Re-registering the same source is harmless. Each `build()` snapshots the current |
| last-registration-wins mapping, so later builder changes do not mutate an existing `ForyJson`. An |
| empty source is a no-op and clears an earlier source for the same target when registered later. |
| |
| A `JsonCodec` supplied by a Mixin is the target's effective annotation. An exact |
| `registerCodec` registration still wins, while the effective type annotation wins over a built-in |
| mapping. |
| |
| On Android, compile a Java-source Mixin for a Java target with the Fory annotation processor. If |
| either the Mixin source or its exact target is Kotlin, apply the Kotlin KSP processor instead so the |
| pair's exact retention rules are packaged. 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. |
| |
| ```java |
| import org.apache.fory.json.annotation.JsonProperty; |
| |
| public final class User { |
| @JsonProperty("user_id") |
| private long id; |
| |
| @JsonProperty(include = JsonProperty.Include.ALWAYS) |
| private String displayName; |
| |
| @JsonProperty(index = 10) |
| private String email; |
| |
| public long getId() { |
| return id; |
| } |
| |
| public void setId(long id) { |
| this.id = id; |
| } |
| } |
| ``` |
| |
| The supported inclusion values are: |
| |
| - `DEFAULT`: use `ForyJsonBuilder.writeNullFields`. |
| - `ALWAYS`: write the property even when its selected value is null. |
| - `NON_NULL`: omit a null value. |
| |
| Inclusion affects writing only. A non-default inclusion is invalid for a creator-only property with |
| no write source. Repeating the same declaration is allowed; conflicting explicit names, indexes, or |
| non-default inclusion policies within one logical property are rejected. Two properties that |
| normalize to the same final JSON name are also rejected. |
| |
| `index` controls relative serialization order. Indexed properties are written in ascending index |
| order before unindexed properties. Indexes must be non-negative, may contain gaps, and must be |
| unique among writable properties. `-1` means unspecified; lower values are invalid. An index on a |
| setter-only, creator-only, or write-ignored property is invalid. |
| |
| `NON_EMPTY`, aliases, formatting, and independent read/write names are not supported. |
| `JsonProperty` cannot be combined with an Any logical property or declared on a `JsonAnySetter`. |
| |
| ## `JsonPropertyOrder` |
| |
| `JsonPropertyOrder` combines a named serialization prefix, property indexes, and final-name |
| alphabetic ordering: |
| |
| ```java |
| import org.apache.fory.json.annotation.JsonProperty; |
| import org.apache.fory.json.annotation.JsonPropertyOrder; |
| |
| @JsonPropertyOrder(value = {"id", "display_name"}, alphabetic = true) |
| public final class User { |
| @JsonProperty(index = 20) |
| public String name; |
| |
| @JsonProperty(value = "display_name", index = 10) |
| public String displayName; |
| |
| public long id; |
| public int age; |
| public String address; |
| } |
| ``` |
| |
| The output order is `id`, `display_name`, `name`, `address`, then `age`. The named prefix is written |
| first, remaining indexed properties follow in ascending index order, and `alphabetic = true` sorts |
| the remaining unindexed properties by final JSON name. Without `alphabetic`, those properties keep |
| their existing relative order. Use `@JsonPropertyOrder(alphabetic = true)` when no named prefix is |
| needed. Alphabetic comparison uses Java's natural, case-sensitive String order and is |
| locale-independent. |
| |
| Order entries match the final JSON name first and the Java logical property name second. The list |
| may be empty only when `alphabetic` is true. Its entries must be non-empty, unique writable |
| properties; unknown and duplicate entries fail when object metadata is built. |
| |
| A subclass declaration replaces both settings from its superclass as a whole. If the subclass has |
| no declaration, the nearest superclass declaration is used and resolved against the subclass |
| properties. Interface declarations are not considered. Ordering affects serialization only; |
| deserialization remains name-based, and subtype discriminators remain before user properties. |
| |
| An unwrapped group also occupies one position, selected by the group's Java logical property name. |
| Its child members remain adjacent and retain the child's own order. |
| |
| A write-enabled `JsonAnyProperty` or `JsonAnyGetter` participates as one position identified by its |
| Java logical property name. The position emits all dynamic entries in Map iteration order: |
| |
| ```java |
| import java.util.Map; |
| import org.apache.fory.json.annotation.JsonAnyProperty; |
| import org.apache.fory.json.annotation.JsonPropertyOrder; |
| |
| @JsonPropertyOrder({"id", "properties", "timestamp"}) |
| public final class Event { |
| public String id; |
| |
| @JsonAnyProperty |
| public Map<String, Object> properties; |
| |
| public long timestamp; |
| } |
| ``` |
| |
| If `properties` contains `x` and `y`, output order is `id`, `x`, `y`, then `timestamp`; no member |
| named `properties` is written. Naming strategies do not transform the Any ordering name. An |
| input-only Any field and `JsonAnySetter` have no write position. Dynamic keys cannot be listed in |
| `JsonPropertyOrder`, and alphabetic ordering never sorts entries inside the Map. |
| |
| ## Property Naming Strategy |
| |
| Configure the naming style for logical properties without an explicit non-empty `JsonProperty` |
| name: |
| |
| ```java |
| import org.apache.fory.json.PropertyNamingStrategy; |
| |
| ForyJson json = |
| ForyJson.builder() |
| .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) |
| .build(); |
| ``` |
| |
| The default `LOWER_CAMEL_CASE` preserves the discovered Java logical property name. `SNAKE_CASE` |
| handles acronym and digit boundaries, for example: |
| |
| - `userName` becomes `user_name`; |
| - `URLValue` becomes `url_value`; |
| - `version2FA` becomes `version2_fa`. |
| |
| A non-empty `@JsonProperty("...")` value, a parameter-local creator name, a subtype discriminator |
| property, and dynamic Any keys are already JSON names and are never transformed. |
| |
| ## `JsonIgnore` |
| |
| `JsonIgnore` is field-targeted and controls the read and write directions of the complete logical |
| property: |
| |
| ```java |
| import org.apache.fory.json.annotation.JsonIgnore; |
| |
| @JsonIgnore(ignoreRead = false, ignoreWrite = true) |
| private String serverManagedValue; |
| ``` |
| |
| Both flags default to true. A same-named getter or setter cannot restore an ignored direction, and |
| `JsonProperty` cannot override it. Fory core's `Expose` annotation has no effect in Fory JSON. |
| |
| ## `JsonValue` |
| |
| `JsonValue` selects one exact `String` field or public zero-argument method as the complete JSON |
| representation of its owning type. Fory writes the selected value as an ordinary JSON string, with |
| quotes and normal escaping, instead of writing the owning object's properties: |
| |
| ```java |
| import org.apache.fory.json.annotation.JsonCreator; |
| import org.apache.fory.json.annotation.JsonValue; |
| |
| public final class UserId { |
| private final String value; |
| |
| @JsonCreator |
| public UserId(String value) { |
| this.value = value; |
| } |
| |
| @JsonValue |
| public String value() { |
| return value; |
| } |
| } |
| ``` |
| |
| `json.toJson(new UserId("user-1"))` returns `"user-1"`. The method need not use a JavaBean getter |
| name. It must be public, non-static, zero-argument, and return exactly `String`; a field must be an |
| eligible non-static instance field. One type may have only one effective value member. An |
| unannotated method override suppresses an inherited declaration. |
| |
| `JsonValue` controls serialization by itself. Deserialization additionally requires a |
| `JsonCreator` constructor or public static factory with exactly one `String` parameter, an empty |
| `JsonCreator.value()`, and no `JsonProperty` on that parameter. Fory recognizes that shape as the |
| reverse String constructor; no creator mode is needed. Existing property-list and parameter-local |
| creator forms are unchanged. Without the matching creator, writing still works and reading the |
| owning type fails clearly. |
| |
| A null owner is written and read as JSON `null` without invoking either member or creator. A |
| non-null owner whose value member returns null is also written as JSON `null`. `JsonValue` does not |
| change Map key encoding. |
| |
| ## `JsonRawValue` |
| |
| `JsonRawValue` marks one fixed ordinary `String` property. Fory writes the String directly at the |
| value position without quotes, escaping, parsing, or validation: |
| |
| ```java |
| import org.apache.fory.json.annotation.JsonRawValue; |
| |
| public final class Response { |
| public int status; |
| |
| @JsonRawValue |
| public String body; |
| } |
| ``` |
| |
| With `status = 200` and `body = "{\"id\":1}"`, the output contains |
| `{"status":200,"body":{"id":1}}`. The raw String may be any complete JSON value, including an |
| object, array, number, boolean, quoted JSON string, or `null` token. |
| |
| This annotation is a trusted write-only escape hatch. Invalid or attacker-controlled content can |
| make the enclosing output invalid or change its structure. Java null still follows the property's |
| normal inclusion policy and, when included, is written as JSON `null`. |
| |
| Reading remains ordinary String-property reading. For example, `{"body":"text"}` can populate the |
| field, but an object such as `{"body":{"id":1}}` cannot be read back into it. `JsonRawValue` is not |
| a type-use annotation and does not apply to container elements or Map values. It cannot be placed |
| on a setter, creator parameter, Any declaration, or the same property occurrence as `JsonCodec`. |
| As an occurrence-local representation, it keeps the raw String shape even when the value type has |
| an exact builder-registered codec. |
| |
| `JsonRawValue` does not collect unknown sibling fields. Unknown fields are skipped unless an |
| existing `JsonAnyProperty` or `JsonAnyGetter`/`JsonAnySetter` owner captures them. The raw-value and |
| Any-property features are independent. |
| |
| `JsonValue` and `JsonRawValue` may be combined on the same String member to write an owning object |
| as a trusted raw root value. That combination is serialization-only: the ordinary one-String |
| `JsonCreator` cannot turn an input object or array into a String. |
| |
| ## `JsonBase64` |
| |
| `JsonBase64` selects a quoted standard Base64 JSON string for one exact `byte[]` field or getter: |
| |
| ```java |
| import org.apache.fory.json.annotation.JsonBase64; |
| |
| public final class Attachment { |
| @JsonBase64 |
| public byte[] content; |
| } |
| ``` |
| |
| Bytes `{1, 2, 3}` are written as `{"content":"AQID"}` and decoded back to the original array. |
| Fory writes the Base64 characters directly to the JSON output and decodes directly from the JSON |
| input without creating an intermediate String. Standard Base64 padding is preserved. Java null |
| follows the property's normal inclusion rule and reads from JSON null as null. |
| |
| The annotation is not a type-use annotation and does not change ordinary unannotated `byte[]` |
| properties, container elements, or Map values. It cannot share a logical property with |
| `JsonRawValue`, an occurrence `JsonCodec`, `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: |
| |
| ```java |
| 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: |
| |
| ```java |
| import org.apache.fory.json.annotation.JsonUnwrapped; |
| |
| public final class Person { |
| public int age; |
| |
| @JsonUnwrapped(prefix = "name_") |
| public Name name; |
| } |
| |
| public final class Name { |
| public String first; |
| public String last; |
| } |
| ``` |
| |
| This maps `Person` to `{"age":18,"name_first":"Ada","name_last":"Lovelace"}`. The |
| optional prefix and suffix apply to each child's final JSON name after `JsonProperty` and the |
| configured naming strategy. Nested unwrapped properties compose their transformations from the |
| inside out. |
| |
| A null child writes no members. On input, Fory creates and assigns the child only after seeing one |
| of its flattened members. A completely missing group therefore preserves a mutable parent's |
| initializer value and leaves a record or creator argument at its normal missing-property default. |
| Partial input constructs the child with the ordinary defaults for its other properties. |
| |
| Mutable classes, records, and `JsonCreator` classes can be parents or children. A parameter-local |
| creator parameter may declare a read-only unwrapped group; its required `JsonProperty` value names |
| the Java creator argument and is not accepted as a JSON wrapper. A parameterized parent is allowed, |
| but every unwrapped child and intermediate must be an exact raw, non-generic class using Fory's |
| standard object mapping. |
| |
| The flattened group occupies one position in the parent's write order. `JsonProperty.index` may |
| position it, and `JsonPropertyOrder` selects it by Java logical property name. The child's own |
| property order remains intact. Parent fields are matched before flattened fields, which are matched |
| before dynamic Any handling. |
| |
| Fory rejects duplicate 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: |
| |
| ```java |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import org.apache.fory.json.annotation.JsonAnyProperty; |
| |
| public final class Event { |
| public String id; |
| |
| @JsonAnyProperty |
| public Map<String, Object> properties = new LinkedHashMap<>(); |
| } |
| ``` |
| |
| For `properties` containing `"source" -> "mobile"`, Fory writes |
| `{"id":"7","source":"mobile"}`, not a nested `properties` member. Unknown input members are |
| inserted into the Map. The field reads and writes by default; `JsonIgnore` may select one direction: |
| |
| ```java |
| import org.apache.fory.json.annotation.JsonIgnore; |
| |
| @JsonAnyProperty |
| @JsonIgnore(ignoreRead = true, ignoreWrite = false) |
| public Map<String, Object> outputOnly; |
| ``` |
| |
| During reading, an existing Map is reused. A null non-final field is initialized when the first |
| unknown member is encountered. A readable final field on an ordinary mutable object must already |
| contain a mutable Map. Records and property-list `JsonCreator` types instead receive the accumulated |
| Map through their construction argument. If no unknown member is present, Fory does not initialize |
| a null field. |
| |
| Use `JsonAnyGetter` and `JsonAnySetter` for method-backed writing and reading: |
| |
| ```java |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import org.apache.fory.json.annotation.JsonAnyGetter; |
| import org.apache.fory.json.annotation.JsonAnySetter; |
| |
| public final class Event { |
| private final Map<String, Object> properties = new LinkedHashMap<>(); |
| |
| @JsonAnyGetter |
| public Map<String, Object> getProperties() { |
| return properties; |
| } |
| |
| @JsonAnySetter |
| public void putProperty(String name, Object value) { |
| properties.put(name, value); |
| } |
| } |
| ``` |
| |
| An any-getter is a public instance method with no arguments and a `Map<String, V>` return type. An |
| any-setter is a public instance method with signature `void method(String, V)`. Either method may be |
| used alone. When paired, their resolved value types must match after primitive types are boxed. A |
| primitive any-setter value parameter rejects JSON null. An any-setter is not supported on records or |
| types that use `JsonCreator`. |
| |
| A read-enabled `JsonAnyProperty` on a record component supplies that component from unknown input |
| members. In property-list `JsonCreator` mode, a read-enabled Any field must correspond to one listed |
| creator argument; parameter-local creator mode cannot bind a field annotation. A write-only Any |
| field or any-getter cannot occupy a creator argument. If a write-only Any field or any-getter claims |
| a record component, that component receives its normal Java default during reading. |
| |
| An any-getter claims its Java logical property: `getProperties()` and `properties()` both claim |
| `properties`. A same-named field, ordinary getter, or ordinary setter is not also mapped as a fixed |
| member. Fory does not infer a differently named backing field, so annotate that field with |
| `JsonIgnore` if it must not be mapped separately. `JsonAnySetter` has no logical property name and |
| does not claim a backing field. |
| |
| The Any logical name is used only for property grouping and `JsonPropertyOrder`; it is not itself a |
| fixed JSON member. An input member with that name is an ordinary dynamic entry rather than a nested |
| aggregate, and the same dynamic output key remains valid unless another fixed property conflicts |
| with it. |
| |
| One effective type hierarchy may use either one `JsonAnyProperty` field or at most one effective |
| `JsonAnyGetter` and one effective `JsonAnySetter`; the forms cannot be mixed. An unannotated method |
| override disables an inherited method annotation. Method-backed Any annotations are invalid in |
| field mode. `JsonProperty` is invalid on an Any setter and on every member of a logical property |
| claimed by an Any field or getter. A same-named field cannot use `JsonIgnore` to suppress an |
| any-getter's write direction. Its `ignoreRead` flag also does not disable a separate any-setter. |
| |
| Dynamic keys are exact JSON member names and retain Map iteration order. A null Map writes no |
| members, and a null Map value writes JSON null regardless of fixed-property null settings. Null and |
| non-String output keys are rejected. Raw Maps, wildcard or unresolved keys, and non-String key |
| types are invalid. Declared fixed members, including members excluded from reading, are not |
| delivered to an Any input. An output key 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: |
| |
| ```java |
| import org.apache.fory.json.annotation.JsonCreator; |
| |
| public final class User { |
| public final long id; |
| public final String name; |
| |
| @JsonCreator({"id", "name"}) |
| public User(long id, String name) { |
| this.id = id; |
| this.name = name; |
| } |
| } |
| ``` |
| |
| The parameter-local form gives every parameter an explicit JSON name. It may introduce |
| creator-only input properties: |
| |
| ```java |
| @JsonCreator |
| public static User create( |
| @JsonProperty("user_id") long id, |
| @JsonProperty("display_name") String name) { |
| return new User(id, name); |
| } |
| ``` |
| |
| Parameter-local names bypass the naming strategy. The two modes cannot be mixed. In compact mode, |
| names must be non-empty and unique, the name count must equal the parameter count, and parameters |
| must not also declare `JsonProperty`. In parameter-local mode, every parameter requires a |
| non-empty, unique `JsonProperty` name. |
| |
| For a type with `JsonValue`, the empty form also accepts exactly one `String` parameter without |
| `JsonProperty` and reconstructs the owning value from its JSON string. This value form is distinct |
| from both property-based forms and is inferred only because the target has `JsonValue`. |
| |
| A creator must have at least one parameter and cannot be varargs or generic. A constructor must be |
| public. A factory must be public and static, declare the target class as its exact return type, and |
| return a non-null value whose runtime class is exactly the target. Missing reference parameters use |
| null, missing primitives use Java zero values, duplicate members use the last value, and explicit |
| null for a primitive parameter is rejected. Records cannot declare a property-based `JsonCreator`; |
| a record with `JsonValue` may annotate its one-String canonical constructor for the value form. |
| |
| ## `JsonValidator` |
| |
| Use `JsonValidator` for application validation that must run after an object has been completely |
| constructed and populated: |
| |
| ```java |
| 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: |
| |
| ```java |
| 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. Post-build subtype registration and open subtype discovery are not supported. |
| |
| The default `PROPERTY` inclusion writes an inline discriminator as the first output member: |
| |
| ```java |
| import org.apache.fory.json.annotation.JsonSubTypes; |
| |
| @JsonSubTypes( |
| property = "kind", |
| value = { |
| @JsonSubTypes.Type(value = Circle.class, name = "circle"), |
| @JsonSubTypes.Type( |
| className = "com.example.shape.Rectangle", |
| name = "rectangle") |
| }) |
| public interface Shape {} |
| ``` |
| |
| ```json |
| { "kind": "circle", "radius": 2 } |
| ``` |
| |
| Property input accepts the discriminator at any direct object-member position, but it must appear |
| exactly once, be a string, and name a configured subtype. The discriminator property bypasses the |
| naming strategy and must not collide with a subtype's ordinary JSON property. Property inclusion |
| requires the subtype's ordinary object representation. |
| |
| `WRAPPER_OBJECT` uses one outer member: |
| |
| ```java |
| @JsonSubTypes( |
| inclusion = JsonSubTypes.Inclusion.WRAPPER_OBJECT, |
| value = {@JsonSubTypes.Type(value = Circle.class, name = "circle")}) |
| public interface Shape {} |
| ``` |
| |
| ```json |
| { "circle": { "radius": 2 } } |
| ``` |
| |
| `WRAPPER_ARRAY` uses exactly two array elements: |
| |
| ```java |
| @JsonSubTypes( |
| inclusion = JsonSubTypes.Inclusion.WRAPPER_ARRAY, |
| value = {@JsonSubTypes.Type(value = Circle.class, name = "circle")}) |
| public interface Shape {} |
| ``` |
| |
| ```json |
| ["circle", { "radius": 2 }] |
| ``` |
| |
| The configuration rules are strict: |
| |
| | Inclusion | `property` | Subtype representation | |
| | ---------------- | ---------------------- | ----------------------------------------------------- | |
| | `PROPERTY` | Required and non-empty | Ordinary object members inline with the discriminator | |
| | `WRAPPER_OBJECT` | Must be empty | Complete subtype value inside one-member object | |
| | `WRAPPER_ARRAY` | Must be empty | Complete subtype value as array element 1 | |
| |
| Both wrappers may delegate to an exact custom subtype codec. All three inclusions write null as |
| plain JSON null unless codec precedence selects a custom complete-value codec for the declared |
| base, replacing the annotation. |
| |
| The base must be an interface or abstract class. Every entry must resolve to a unique concrete, |
| assignable class, and serialization accepts only an exact listed runtime class. Listing a parent |
| does not implicitly admit its descendants. The annotation is read from the declared base itself and |
| is not inherited from another annotated interface or abstract class. Readers accept only the |
| configured inclusion; changing inclusion is a wire-format change and there is no dual-read |
| fallback. |
| |
| At GraalVM native-image runtime, annotate the base with `JsonType` and use class-literal entries |
| rather than `className` entries. Listed class-literal subtypes are registered automatically. |