title: Custom Codecs sidebar_position: 5 id: custom-codecs 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
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();
Use JsonCodecFactory when one factory owns a family of declared or parameterized types:
import org.apache.fory.json.JsonCodecFactory; JsonCodecFactory factory = (type, resolver, runtimeType) -> type.getRawType() == Money.class ? new MoneyCodec() : null; ForyJson json = ForyJson.builder() .withModule(context -> context.registerCodecFactory(factory)) .build();
runtimeType is true only when the factory is selecting a codec for the actual class of a value during a dynamic write. Declared roots and composite child types receive false. A composite codec that needs this distinction after construction must retain the flag for its later resolveTypes call; it must not infer the value from resolver state.
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.
A custom codec that materializes composite graph owners must call JsonReader.reserveGraphMemory with its application-defined byte estimate before creating each owner. This applies to composite application objects, collections, maps, and reference arrays. Reserve collection and map reference storage before the mutation that may grow it. A custom scalar or other dedicated leaf representation, such as MoneyCodec above, does not reserve graph memory. Complete codecs also perform their own application validation; Fory JSON does not wrap a complete custom representation with the target type's JsonValidator methods.
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.
JsonCodecUse @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:
| Member | Supported current value | Direct child handled by the codec |
|---|---|---|
elementCodec | Collection<E>, E[], AtomicReferenceArray<E> | E |
contentCodec | Optional<T>, AtomicReference<T> | T |
keyCodec | Map<K, V> | JSON member name for K |
valueCodec | Map<K, V> | direct V value |
Kotlin uses the same codec registrations and JsonCodec annotation. Apply property annotations to an explicit supported use site, for example:
import org.apache.fory.json.annotation.JsonCodec data class Ledger( @field:JsonCodec(value = MoneyCodec::class) val total: Money, @field:JsonCodec(elementCodec = MoneyCodec::class) val entries: List<Money>, )
The complete-value codec owns the whole JSON value. Child codecs leave the standard array, collection, Optional/atomic, or map representation in control. Kotlin nullability is still the declared occurrence contract around a selected application codec: after a non-null JSON token, the codec must return the exact declared type and must not return null for a non-null occurrence.
An unsigned or eligible value-class map key can use the built-in member-name mapping without an annotation. Use an explicit keyCodec or whole-map codec when the key has a different tagged text shape. An exact application registration still takes precedence over the Kotlin module defaults.
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.
Fory resolves each current value in this order:
| Priority | Source |
|---|---|
| 1 | Current property or parameter JsonCodec |
| 2 | Exact registerCodec registration |
| 3 | Exact-target type JsonCodec after Mixin overlay |
| 4 | Inherited-frontier type JsonCodec declaration |
| 5 | Built-in or default JSON mapping |
One logical property may expose the annotation from its field, getter, setter parameter, creator parameter, or record propagation. Repeated configurations must be identical; Fory does not merge partial configurations from different declarations. An unannotated effective override suppresses an inherited method annotation.
A child member replaces only that direct child. Unconfigured Map siblings continue through the normal precedence. If an exact registration or type declaration supplies a complete codec for the outer container, a property child member is unreachable and therefore rejected.
Map keys are JSON object member names and use MapKeyCodec, not JsonValueCodec. A custom key codec class follows the same construction rules as a value codec. Null Map keys are rejected, and decoded keys must match the declared key type.
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.
Outside GraalVM Native Image, a named Java module must export or open the codec package to org.apache.fory.json. Native Image prepares annotation-codec constructors during image construction and does not require that package access. 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.