DEV Community

hiro@
hiro@

Posted on Originally published at qiita.com

Comparing structuredClone and JSON copies through values and reference identity

Copying an object with JSON.parse(JSON.stringify(value)) can succeed while changing what the data means. In this experiment, the changes went beyond turning a Date into a string: Map contents and relationships between references changed too.

The two approaches were compared on Node.js 24.15.0. Specify which values need copying and which reference relationships the copy must retain.

Check values and references together

This example uses synthetic data resembling an article draft, not real user records. The experiment ran on September 11, 2026. The JSON roundtrip used no replacer, reviver, or custom toJSON method.

const row = { id: 'draft-1', status: 'ready' };
const source = {
  createdAt: new Date('2026-09-11T00:00:00.000Z'),
  counts: new Map([['ready', 2]]),
  memo: undefined,
  slots: [undefined],
  left: row,
  right: row,
};
const cloned = structuredClone(source);
const jsonCopy = JSON.parse(JSON.stringify(source));
console.log(cloned.createdAt instanceof Date, typeof jsonCopy.createdAt);
console.log(cloned.counts.get('ready'), jsonCopy.counts);
console.log(Object.hasOwn(cloned, 'memo'), Object.hasOwn(jsonCopy, 'memo'));
console.log(cloned.slots[0], jsonCopy.slots[0]);
console.log(cloned.left === cloned.right, jsonCopy.left === jsonCopy.right);
Enter fullscreen mode Exit fullscreen mode

It printed:

true string
2 {}
true false
undefined null
true false
Enter fullscreen mode Exit fullscreen mode

The structured clone's Date and Map remained usable as those types. The JSON copy contained a string for the Date and an empty object for the Map. Its memo: undefined property disappeared, while undefined inside the array became null.

The final line matters for some copying requirements. Originally, left and right referenced the same row. The structured clone kept them pointing at the same copied object. The JSON roundtrip made separate objects. Neither approach retained a reference to the original row. The HTML structured serialization specification defines bookkeeping that avoids processing an object twice and preserves cycles and reference identity.

Success alone does not establish the intended copy semantics

The experiment tested 12 separate inputs. These are representative results:

Input structuredClone JSON roundtrip
Date Date ISO-formatted string
Map Map containing entries Empty object
undefined in an object Key and value retained Key removed
undefined in an array undefined element null element
NaN, Infinity, -0 Properties of each value retained null, null, 0
Two properties referencing the same child Copied properties share a child Copied properties have separate children
Circular reference Cycle retained in the copy TypeError
Object containing BigInt bigint value retained TypeError
Object with a function property DataCloneError Function key removed

The JSON cases that removed a function or undefined property did not throw. A test that checks only for exceptions misses those changes. The ECMAScript JSON specification also distinguishes serialization by value type and by its position in an object or array.

The comparison used instanceof, Object.hasOwn, Object.is, and reference === checks. Comparing only JSON strings cannot test properties that serialization itself removes.

structuredClone does not fully duplicate class instances

The remaining 3 cases examined a class, frozen state, and a getter.

Copying a custom Draft instance retained its ordinary title data property. However, instanceof Draft was false, and the prototype's label() method was absent. The JSON roundtrip produced the same observations.

Copies made from Object.freeze({ count: 1 }) were not frozen under either method, and count was writable. For an object with a getter, both methods invoked it while reading the value. The copy contained an ordinary property with the value 2. Copying once with each method invoked the getter a total of 2 times.

These results support using class-specific construction when the goal includes reproducing a domain object's behavior. Copying this class did not produce another instance with the same callable methods.

Transfer does not leave the original buffer available

const bytes = new Uint8Array([1, 2, 3]);
const moved = structuredClone(bytes, { transfer: [bytes.buffer] });
console.log(bytes.byteLength, [...moved]);
Enter fullscreen mode Exit fullscreen mode

This printed 0 [ 1, 2, 3 ]. The destination retained the values, while the original ArrayBuffer was detached. If another operation still needs the source buffer, this cannot be treated as an ordinary copy. Node.js structuredClone provides the WHATWG API with this transfer option.

The experiment measured differences in values, references, and exceptions. It did not compare speed, memory use for large inputs, or browser-specific objects such as DOM elements. A protocol that exchanges JSON still needs conversion to that format. This experiment helps identify which changes are acceptable when reusing a JSON roundtrip for an in-memory copy.

The execution script and results for the 12 cases and transfer record the shape of each copy as well as whether copying succeeded.

Top comments (0)