custom-serializers.md

June 5, 2026 ยท View on GitHub

This page covers the current Java custom serializer API.

Constructor Inputs

Custom serializers should not retain Fory.

  • Use Config when the serializer only depends on immutable configuration and can be shared.
  • Use TypeResolver when the serializer needs type metadata, generics, or nested dynamic dispatch.
  • If a serializer retains TypeResolver, it is usually not shareable and should not implement Shareable.

Basic Serializer

Use WriteContext and ReadContext for per-operation state. Only get the buffer into a local variable when you perform multiple reads or writes.

import org.apache.fory.config.Config;
import org.apache.fory.context.ReadContext;
import org.apache.fory.context.WriteContext;
import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.serializer.Serializer;
import org.apache.fory.serializer.Shareable;

public final class FooSerializer extends Serializer<Foo> implements Shareable {
  public FooSerializer(Config config) {
    super(config, Foo.class);
  }

  @Override
  public void write(WriteContext writeContext, Foo value) {
    writeContext.getBuffer().writeInt64(value.f1);
    writeContext.writeString(value.f2);
  }

  @Override
  public Foo read(ReadContext readContext) {
    MemoryBuffer buffer = readContext.getBuffer();
    Foo foo = new Foo();
    foo.f1 = buffer.readInt64();
    foo.f2 = readContext.readString(buffer);
    return foo;
  }
}

Register it with a Config-based constructor when the serializer is shareable:

Fory fory = Fory.builder().withXlang(false).build();
fory.registerSerializer(Foo.class, new FooSerializer(fory.getConfig()));

Nested Objects

If your serializer needs to write or read nested objects, use the context helpers instead of retaining Fory:

import org.apache.fory.config.Config;
import org.apache.fory.context.ReadContext;
import org.apache.fory.context.WriteContext;
import org.apache.fory.serializer.Serializer;

public final class EnvelopeSerializer extends Serializer<Envelope> {
  public EnvelopeSerializer(Config config) {
    super(config, Envelope.class);
  }

  @Override
  public void write(WriteContext writeContext, Envelope value) {
    writeContext.writeRef(value.header);
    writeContext.writeRef(value.payload);
  }

  @Override
  public Envelope read(ReadContext readContext) {
    Envelope envelope = new Envelope();
    envelope.header = (Header) readContext.readRef();
    envelope.payload = readContext.readRef();
    return envelope;
  }
}

This serializer can implement Shareable because it retains no Fory-instance-local mutable state.

Collection Serializers

For Java collections, extend CollectionSerializer or CollectionLikeSerializer.

  • Use CollectionSerializer for real Collection implementations.
  • Use CollectionLikeSerializer for collection-shaped types that do not implement Collection.
  • Keep supportCodegenHook == true when the collection can use the standard element codegen path.
  • Set supportCodegenHook == false only when you need to fully control element IO.

Example:

import java.util.ArrayList;
import java.util.Collection;
import org.apache.fory.context.ReadContext;
import org.apache.fory.context.WriteContext;
import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.resolver.TypeResolver;
import org.apache.fory.serializer.collection.CollectionSerializer;

public final class CustomCollectionSerializer<T extends Collection<?>>
    extends CollectionSerializer<T> {
  public CustomCollectionSerializer(TypeResolver typeResolver, Class<T> type) {
    super(typeResolver, type, true);
  }

  @Override
  public Collection onCollectionWrite(WriteContext writeContext, T value) {
    writeContext.getBuffer().writeVarUInt32Small7(value.size());
    return value;
  }

  @Override
  public T onCollectionRead(Collection collection) {
    return (T) collection;
  }

  @Override
  public Collection newCollection(ReadContext readContext) {
    MemoryBuffer buffer = readContext.getBuffer();
    int numElements = buffer.readVarUInt32Small7();
    setNumElements(numElements);
    return new ArrayList(numElements);
  }
}

Map Serializers

For Java maps, extend MapSerializer or MapLikeSerializer.

import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.context.ReadContext;
import org.apache.fory.context.WriteContext;
import org.apache.fory.resolver.TypeResolver;
import org.apache.fory.serializer.collection.MapSerializer;

public final class CustomMapSerializer<T extends Map<?, ?>> extends MapSerializer<T> {
  public CustomMapSerializer(TypeResolver typeResolver, Class<T> type) {
    super(typeResolver, type, true);
  }

  @Override
  public Map onMapWrite(WriteContext writeContext, T value) {
    writeContext.getBuffer().writeVarUInt32Small7(value.size());
    return value;
  }

  @Override
  public T onMapRead(Map map) {
    return (T) map;
  }

  @Override
  public Map newMap(ReadContext readContext) {
    MemoryBuffer buffer = readContext.getBuffer();
    int numElements = buffer.readVarUInt32Small7();
    setNumElements(numElements);
    return new LinkedHashMap(numElements);
  }
}

Registration

Fory fory = Fory.builder().withXlang(false).build();

fory.registerSerializer(Foo.class, new FooSerializer(fory.getConfig()));
fory.registerSerializer(
    CustomMap.class, new CustomMapSerializer<>(fory.getTypeResolver(), CustomMap.class));
fory.registerSerializer(
    CustomCollection.class,
    new CustomCollectionSerializer<>(fory.getTypeResolver(), CustomCollection.class));

If you want Fory to construct the serializer lazily, register a factory:

fory.registerSerializer(
    CustomMap.class, resolver -> new CustomMapSerializer<>(resolver, CustomMap.class));

Shareability

Implement the Shareable marker interface when the serializer can be safely reused across equivalent Fory instances and concurrent operations. A shareable serializer must not retain operation state, Fory-instance-local mutable state, or mutable scratch buffers shared across calls. Consumers can check shareability via serializer instanceof Shareable.

In practice:

  • Config-only serializers are often shareable.
  • TypeResolver-based serializers are usually not shareable.
  • Operation state belongs in WriteContext, ReadContext, and CopyContext, not in serializer fields.