Skip to main content
Version: dev

Object Copy

This page covers in-memory Java object graph copying with Fory#copy(Object).

Fory.copy is a deep-copy operation for Java object graphs. It does not serialize to bytes first. Instead, it uses the same runtime type system and serializers to create a copied object graph in memory.

When to Use Object Copy

Use object copy when you want a detached in-memory clone of an existing Java object graph.

Typical use cases:

  • Clone request or response models before mutation
  • Duplicate cached state for optimistic updates
  • Copy graphs that contain collections, maps, arrays, or nested beans
  • Preserve shared references and circular references during cloning

Use serialization instead when you need bytes for transport, storage, or cross-process exchange.

OperationFory.copyserialize / deserialize
ResultJava object graphBinary payload plus reconstructed objects
Main useIn-memory deep copyTransport, persistence, interoperability
Copy ref optionwithRefCopy(...)withRefTracking(...)
Cross-language payloadNoYes, in xlang mode
Intermediate byte bufferNoYes

Quick Start

For general-purpose object graphs, enable withRefCopy(true) so shared references and cycles are handled correctly:

import org.apache.fory.Fory;
import org.apache.fory.config.Language;

public class Example {
public static void main(String[] args) {
Fory fory = Fory.builder()
.withLanguage(Language.JAVA)
.withRefCopy(true)
.build();

Order original = new Order();
Order copied = fory.copy(original);
}
}

copy(null) returns null.

Reference Semantics

The most important copy option is ForyBuilder#withRefCopy(boolean).

withRefCopy(true)

This is the safe default for general object graphs. Shared references remain shared in the copied graph, and circular references can be copied correctly.

import org.apache.fory.Fory;
import org.apache.fory.config.Language;

public class Example {
static final class Address {
String city;
}

static final class Pair {
Address left;
Address right;
}

public static void main(String[] args) {
Fory fory = Fory.builder()
.withLanguage(Language.JAVA)
.withRefCopy(true)
.build();

Address address = new Address();
address.city = "Shanghai";

Pair pair = new Pair();
pair.left = address;
pair.right = address;

Pair copied = fory.copy(pair);
System.out.println(copied.left == copied.right); // true
}
}

withRefCopy(false)

Disable copy ref tracking only when you know the graph is tree-like and does not rely on shared or cyclic references. This can be faster, but repeated references are copied into different objects.

import org.apache.fory.Fory;
import org.apache.fory.config.Language;

public class Example {
static final class Address {
String city;
}

static final class Pair {
Address left;
Address right;
}

public static void main(String[] args) {
Fory fory = Fory.builder()
.withLanguage(Language.JAVA)
.withRefCopy(false)
.build();

Address address = new Address();
Pair pair = new Pair();
pair.left = address;
pair.right = address;

Pair copied = fory.copy(pair);
System.out.println(copied.left == copied.right); // false
}
}

If you disable withRefCopy and the graph contains a cycle, copy can fail with stack overflow.

withRefCopy vs withRefTracking

These two options control different operations:

  • withRefCopy(true) affects Fory.copy(...)
  • withRefTracking(true) affects serialization and deserialization

Enabling one does not automatically enable the other. If your application both serializes and copies graphs with shared or circular references, configure both options explicitly.

Fory fory = Fory.builder()
.withRefTracking(true)
.withRefCopy(true)
.build();

Immutable vs Mutable Values

Fory may reuse the original instance for immutable values. For mutable values, it creates a new object graph.

In practice, this means:

  • String, boxed primitives, enums, and many immutable JDK value types may be returned as-is
  • Primitive arrays, string arrays, collections, maps, beans, dates, and other mutable structures are copied into distinct objects

Do not use object identity alone to decide whether copy succeeded. Use the mutability contract of the value you are copying.

Class Registration

If class registration is required, register copied classes before calling copy.

import org.apache.fory.Fory;

public class Example {
public static void main(String[] args) {
Fory fory = Fory.builder()
.requireClassRegistration(true)
.withRefCopy(true)
.build();

fory.register(Order.class);
Order copied = fory.copy(new Order());
}
}

This follows the same registration rules as the rest of the runtime: if the runtime requires class registration, copied runtime types must be registered first.

Thread-Safe Copy

ThreadSafeFory also supports copy(...).

For general multi-threaded usage:

import org.apache.fory.Fory;
import org.apache.fory.ThreadSafeFory;
import org.apache.fory.config.Language;

public class Example {
public static void main(String[] args) {
ThreadSafeFory fory = Fory.builder()
.withLanguage(Language.JAVA)
.withRefCopy(true)
.buildThreadSafeFory();

Order copied = fory.copy(new Order());
}
}

The same API also works for buildThreadLocalFory() and buildThreadSafeForyPool(poolSize).

Built-In Coverage

Fory already provides copy support for many common Java runtime types, including:

  • Primitive values and boxed primitives
  • Strings and primitive arrays
  • Common JDK collections and maps
  • Java time and date/time values
  • Beans, records, and nested object graphs

If the runtime already knows how to serialize a mutable type, it may still need an explicit copy implementation in that serializer. For mutable serializers, the default Serializer.copy(...) throws UnsupportedOperationException unless the serializer overrides it.

Custom Copy with ForyCopyable

If a type needs custom copy logic, implement ForyCopyable<T>.

This is the simplest approach when the class itself should control how nested fields are copied:

import java.util.ArrayList;
import java.util.List;
import org.apache.fory.ForyCopyable;
import org.apache.fory.context.CopyContext;

public final class Node implements ForyCopyable<Node> {
private String name;
private final List<Node> neighbors = new ArrayList<>();

@Override
public Node copy(CopyContext copyContext) {
Node copied = new Node();
copyContext.reference(this, copied);
copied.name = name;
for (Node neighbor : neighbors) {
copied.neighbors.add(copyContext.copyObject(neighbor));
}
return copied;
}
}

Guidelines:

  • Call copyContext.reference(origin, copy) immediately after creating a composite mutable object if the type can participate in cycles or shared-reference graphs
  • Use copyContext.copyObject(...) for nested values instead of manually duplicating nested copy logic
  • Keep copy logic consistent with the normal runtime semantics of the type

Custom Copy in a Serializer

When a type already uses a custom serializer, override Serializer.copy(...) for mutable values.

import org.apache.fory.config.Config;
import org.apache.fory.context.CopyContext;
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 Envelope copy(CopyContext copyContext, Envelope value) {
Envelope copied = new Envelope();
copyContext.reference(value, copied);
copied.header = copyContext.copyObject(value.header);
copied.payload = copyContext.copyObject(value.payload);
return copied;
}

@Override
public void write(WriteContext writeContext, Envelope value) {
throw new UnsupportedOperationException("omitted");
}

@Override
public Envelope read(ReadContext readContext) {
throw new UnsupportedOperationException("omitted");
}
}

Use this approach when copy behavior belongs with a serializer rather than the domain class.

Best Practices

  • Reuse Fory or ThreadSafeFory instances instead of rebuilding them for each copy
  • Enable withRefCopy(true) unless you are certain the graph is acyclic and does not rely on shared references
  • Treat withRefCopy(false) as a performance optimization for tree-like data, not as a default
  • Test custom copy implementations with both shared-reference and cyclic graphs
  • Keep mutable custom serializer copy paths explicit and do not rely on fallback behavior

Troubleshooting

Stack Overflow or Copy Failure on Cyclic Graphs

If copy fails on a cyclic object graph, enable withRefCopy(true):

Fory fory = Fory.builder()
.withRefCopy(true)
.build();

Disabling copy ref tracking is only safe for acyclic graphs.

Shared References Are Not Preserved

If the same source object is copied into multiple distinct target objects, withRefCopy is disabled. Turn it on:

Fory fory = Fory.builder()
.withRefCopy(true)
.build();

withRefTracking(true) alone does not change Fory.copy(...) behavior.

Copy for ... is not supported

This means the mutable serializer for that type does not implement copy(...).

Fix it by either:

  • Implementing ForyCopyable<T> on the class, or
  • Overriding Serializer.copy(CopyContext, T) in the registered serializer

Registration Errors

If your runtime uses requireClassRegistration(true), make sure the copied runtime types are registered before calling copy(...).