| title | Object Copy |
|---|---|
| sidebar_position | 9 |
| id | object_copy |
| 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. |
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.
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.
| Operation | Fory.copy |
serialize / deserialize |
|---|---|---|
| Result | Java object graph | Binary payload plus reconstructed objects |
| Main use | In-memory deep copy | Transport, persistence, interoperability |
| Copy ref option | withRefCopy(...) |
withRefTracking(...) |
| Cross-language payload | No | Yes, in xlang mode |
| Intermediate byte buffer | No | Yes |
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.
The most important copy option is ForyBuilder#withRefCopy(boolean).
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
}
}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.
These two options control different operations:
withRefCopy(true)affectsFory.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();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.
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.
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).
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.
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
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.
- Reuse
ForyorThreadSafeForyinstances 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
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.
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.
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
If your runtime uses requireClassRegistration(true), make sure the copied runtime types are
registered before calling copy(...).
- Basic Serialization - Runtime creation and core APIs
- Configuration - Builder options including
withRefCopy - Custom Serializers - Serializer design and registration
- Virtual Threads - Thread-safe runtime guidance