Structured Clone vs JSON for Browser State
Run a native browser corpus through JSON, structured cloning, and transfer to expose type, graph, error, and ownership semantics.
Structured clone vs JSON is a boundary choice: preserve a browser object graph, or produce interoperable text with deliberately narrow types. This comparison runs the same Date, Map, Set, BigInt, typed-array, alias, cycle, error, function, and transferable fixtures in the executing browser.
Structured clone vs JSON starts with the boundary
A browser data copy inside one origin has different needs from a payload that must cross an HTTP API, enter a log, survive manual inspection, or remain readable by another language. Structured cloning preserves many platform values and graph relationships; JSON serialization produces text under a smaller, widely implemented data model.
The WHATWG structured data algorithms define structured serialization behavior used by browser features. The algorithm is not a generic persistence guarantee for every host object, and supported platform types can depend on the destination boundary.
The runnable structured clone vs JSON lab reports semantics observed in the executing browser. It intentionally avoids timing and memory conclusions, because one tiny corpus cannot establish universal performance across engines, payload shapes, warm-up states, or transfer paths.
The constructed browser corpus now spans Date, Map, Set, BigInt, Uint8Array, Error, function, and a repeated-reference graph. Every row has an expected JSON type, expected clone type, status, error name, and schema-pass flag.
Inventory value types before choosing a copy format
List primitives, arrays, plain records, dates, maps, sets, big integers, regular expressions, errors, typed arrays, blobs, files, cyclic references, repeated references, and unsupported executable values. Mark which types must retain behavior, which may become explicit records, and which should be rejected.
The ECMAScript JSON object describes parse and stringify semantics. JSON has no native syntax for BigInt, Map, Set, Date, cycles, aliases, typed-array identity, or Error internals; replacers and tagged schemas can encode selected cases, but then the application owns that protocol.
Structured clone vs JSON should compare the product's actual value grammar instead of a table copied from memory. One stray function, proxy, DOM node, or resource handle can turn a seemingly clonable state tree into a runtime exception.
Aliasing is tested with two properties pointing to the same source object. The structured copy must preserve that internal identity, while equality and identity are reported as different outcomes rather than collapsed into a generic success value.
| Date | JSON String | clone Date |
|---|---|---|
| BigInt | throws | clone BigInt |
| Error | plain Object | clone TypeError |
| function | throws in fixture | DataCloneError |
| alias | plain graph | identity preserved |
Preserve graph identity only when it is meaningful
A repeated reference and two equal objects are not the same graph. Structured cloning can reconstruct aliasing and cycles in supported graphs, while naive JSON either duplicates repeated values or throws on a cycle; that difference matters when identity carries application meaning.
Do not preserve identity accidentally. If a draft is intended as an immutable snapshot, shared mutable references can make reasoning harder even when the cloning algorithm reproduces them correctly. Normalize domain state into explicit identifiers when relationships need to survive storage, synchronization, and debugging.
The structured clone vs JSON fixture creates a self-cycle and checks that the cloned reference points back to the clone. This browser-observed result demonstrates one graph invariant, not every exotic object or cross-realm behavior.
The Error row verifies that the executing browser returns a TypeError object. The function row expects a DataCloneError, giving unsupported behavior its own successful assertion instead of treating every thrown exception as a harness failure.
Runnable artifact — Run one frozen value graph through JSON, structuredClone, and transfer, then inspect types, cycles, detachment, and exceptions.
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Structured clone vs JSON lab</title><style>:root{color-scheme:dark}*{box-sizing:border-box}body{font:16px/1.5 system-ui;max-width:960px;margin:auto;padding:24px;background:#101b24;color:#f5f3ea}button,select,a{font:inherit;padding:.72rem 1rem;margin:.35rem;border:2px solid #7ce5c3;border-radius:.6rem;background:#172b36;color:#fff}button:focus-visible,select:focus-visible,a:focus-visible{outline:4px solid #ffd66b;outline-offset:3px}canvas{width:100%;height:auto;border:1px solid #78909c;background:#081116}output{display:block;white-space:pre-wrap;padding:1rem;background:#081116;border-radius:.6rem;margin-top:1rem;overflow-wrap:anywhere}small{display:block;color:#b8cbd4}.exports{display:flex;flex-wrap:wrap;gap:.5rem}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}</style><main><h1>Structured clone vs JSON lab</h1><p>The executing browser runs one frozen value corpus and reports equality separately from graph identity.</p><button id="run">Run value corpus</button><output id="receipt" aria-live="polite"></output></main><script>const shared={label:"shared"},alias={left:shared,right:shared},values={date:new Date("2026-09-01T00:00:00Z"),map:new Map([["mint",2]]),set:new Set([3,5]),big:7n,bytes:new Uint8Array([1,2,3]),error:new TypeError("fixture"),fn:function fixture(){},alias};const schema={date:["String","Date"],map:["Object","Map"],set:["Object","Set"],big:["throws","BigInt"],bytes:["Object","Uint8Array"],error:["Object","TypeError"],fn:["throws","throws"],alias:["Object","Object"]};function attemptJson(value){try{return{status:"ok",value:JSON.parse(JSON.stringify(value))}}catch(error){return{status:"throws",error:error.name}}}function attemptClone(value){try{return{status:"ok",value:structuredClone(value)}}catch(error){return{status:"throws",error:error.name}}}function execute(){const rows=[];for(const [name,value] of Object.entries(values)){const json=attemptJson(value),clone=attemptClone(value),jsonType=json.status==="throws"?"throws":json.value?.constructor?.name||typeof json.value,cloneType=clone.status==="throws"?"throws":clone.value?.constructor?.name||typeof clone.value,equality=name==="date"&&json.status==="ok"?json.value===value.toISOString():name==="big"||name==="fn"?null:json.status==="ok",identity=name==="alias"&&clone.status==="ok"?clone.value.left===clone.value.right:null;rows.push({name,json:{status:json.status,type:jsonType,error:json.error||null},clone:{status:clone.status,type:cloneType,error:clone.error||null},equality,identity,schemaPass:schema[name][0]===jsonType&&schema[name][1]===cloneType})}const cycle={name:"cycle"};cycle.self=cycle;const jsonCycle=attemptJson(cycle),copied=structuredClone(cycle),buffer=new ArrayBuffer(8),moved=structuredClone(buffer,{transfer:[buffer]});const data={path:"browser-native-structuredClone",rows,aliasIdentity:rows.find(x=>x.name==="alias").identity,errorPreserved:rows.find(x=>x.name==="error").clone.type==="TypeError",functionRejected:rows.find(x=>x.name==="fn").clone.status==="throws",jsonCycleError:jsonCycle.error,cloneCycle:copied.self===copied,detachedBytes:buffer.byteLength,receivedBytes:moved.byteLength,schemaComplete:rows.length===Object.keys(schema).length&&rows.every(x=>x.schemaPass)};const pass=data.aliasIdentity&&data.errorPreserved&&data.functionRejected&&data.cloneCycle&&data.detachedBytes===0&&data.schemaComplete;receipt.dataset.execution=JSON.stringify(data);receipt.value=(pass?"PASS: ":"FAIL: ")+JSON.stringify(data,null,2)}run.onclick=execute;execute()</script></html>
Use transfer when ownership should move
Transferable objects let selected resources move to a receiving context instead of copying their backing storage. For an ArrayBuffer, successful transfer detaches the source, making ownership change observable and preventing two contexts from mutating the same bytes independently.
Model that lifecycle as sender owns, transfer begins, sender detached, receiver owns, work completes or cancellation resolves. A fallback copy must receive a different label because it retains source bytes and can have different memory pressure and cleanup requirements.
Structured clone vs JSON has no JSON equivalent to browser-native transfer semantics. When cross-tab work is involved, pair this decision with the BroadcastChannel versus SharedWorker comparison so message transport, process ownership, and value serialization are reviewed together.
Cycles remain explicit: JSON serialization records its TypeError, while structured cloning must return a graph whose self property points back to the copied root. Both branches are required for structured clone vs JSON parity. The returned self-reference is checked by identity, not by printed resemblance.
- Repeated reference
- The cloned left and right properties point to one cloned object.
- Object equality
- The cloned shared object is not the original source object.
- Cycle
- The copied self property points to the copied root.
- JSON
- The cyclic graph throws TypeError before producing text.
Align IndexedDB state with its clone semantics
IndexedDB uses a structured-clone-based value path, which makes rich browser data convenient but does not remove schema design. The IndexedDB clone-value steps are the primary reference; test keys, indexes, transactions, version upgrades, and error paths separately from in-memory cloning.
For an offline draft queue, store a versioned domain envelope with explicit identifiers, timestamps, retry state, and migration rules. Avoid depending on class prototypes or methods that will not reconstruct as application behavior after persistence.
Structured clone vs JSON at this boundary may legitimately favor cloning for storage and JSON for server synchronization. Name the conversion between those representations so dropped types or normalized values cannot appear as an unexplained round-trip bug.
Transfer uses an eight-byte ArrayBuffer and asserts two postconditions. The receiving buffer keeps eight bytes and the original buffer reports zero, demonstrating ownership movement rather than ordinary copying. That detachment receipt distinguishes transfer semantics from a successful deep clone.
Design failure as part of the state contract
JSON.stringify can throw on BigInt and cycles; structuredClone can throw DataCloneError for unsupported inputs. Catch those failures at the boundary, retain the last valid state, identify the offending field when safe, and offer a domain-level repair rather than silently dropping data.
Functions and capabilities should not travel inside state snapshots. Replace them with named commands or identifiers resolved under current authority, especially when a worker, frame, or storage record might otherwise appear to carry executable trust.
The structured clone vs JSON lab records exception names beside successful type results. That receipt makes unsupported cases first-class and prevents a demo from showing only values selected because both mechanisms happen to accept them.
The table schema is exhaustive over the frozen corpus. Adding a new type without an expectation makes schemaComplete false, so attractive output cannot hide an unreviewed row. Both error names remain inspectable. Unknown constructors must earn a new explicit expectation before the matrix can pass.
- Create an eight-byte ArrayBuffer.
- Call structuredClone with the buffer in the transfer list.
- Assert the source byteLength is zero.
- Assert the received byteLength is eight.
- Do not label ordinary copying as transfer.
Choose text when interoperability owns the requirement
JSON is often right when the output must cross languages, pass through HTTP infrastructure, enter version control, support human inspection, or remain stable under an explicit schema. Add a version, validate on both sides, define numeric limits, and decide how dates, binary data, and large integers are represented.
Structured cloning is often right for in-browser messaging, isolated copies, IndexedDB values, and ownership transfer where the platform semantics match the domain. It is not a wire format, and a successful clone does not produce a portable artifact for another runtime.
If durable browser data moves into SQLite or OPFS, review structured local persistence separately. Structured clone vs JSON decides the immediate representation boundary, not the entire storage engine or synchronization architecture.
These observations describe the browser that executed the lab. They do not compare speed, memory pressure, IndexedDB persistence, worker scheduling, or wire compatibility, all of which require different boundaries and measurements.
Ship a corpus-backed serialization decision
Run dates, maps, sets, BigInt, typed arrays, aliases, cycles, errors, unsupported functions, and transferables in every supported browser channel. Archive user agent, path, input manifest, type observations, equality checks, identity checks, detachment, exceptions, and the domain policy derived from them.
For history that must be reversible and portable, JSON Patch undo and redo can be a better command representation than cloning whole states. That choice carries its own pointer, inverse, array, and conflict contracts.
Structured clone vs JSON is resolved when the team writes one sentence about the boundary, one allowed value grammar, one failure policy, and one interoperability requirement. Run the browser lab, then replace its generic corpus with sanitized product values before final adoption.
A storage design should still version its durable schema. Structured cloning can preserve rich types across browser boundaries, but that convenience does not make opaque object graphs interoperable with non-browser systems. Migration code should describe the old graph shape before writing its replacement.
Version custom encodings as real protocols
Teams often add JSON replacers and revivers for dates, big integers, binary values, maps, or domain classes. Once those tags leave one function, they form a protocol: reserve a namespace, define allowed payloads, validate before revival, reject unknown versions, and protect ordinary user objects from accidentally matching a privileged tag.
Test malicious and malformed tags, missing fields, huge lengths, duplicate keys, numeric overflow, and a future version. Revival should create plain validated data before any domain constructor runs, because parsing untrusted text must never become an ambient capability to instantiate arbitrary application objects.
Structured clone vs JSON can still favor JSON with a tagged schema when portability matters, but the comparison must count the maintenance cost honestly. Document migrations, canonical output when signing or hashing is required, and the downgrade behavior for older clients rather than treating a replacer as a free extension to the standard.
The value corpus is small enough to run after browser updates. A changed constructor, error name, or transferable outcome becomes a visible receipt diff rather than an inferred compatibility statement.
Rehearse upgrades and rollback at stored boundaries
Persisted state outlives the JavaScript that wrote it. Build fixtures from every supported schema version, open them under the candidate application, verify deterministic migration, close and reopen, then confirm rollback either remains supported or is explicitly blocked before new writes occur.
For cloned IndexedDB values, migration may need to normalize legacy Dates, Maps, typed arrays, or records into a current domain envelope. For JSON, preserve raw source on failure and avoid partial rewrites; use a transaction or copy-on-write path so one malformed record cannot strand the entire store in a mixed version.
Structured clone vs JSON is operationally complete only after corrupted data, quota errors, interrupted migrations, and old-client access have named outcomes. The happy-path browser lab selects semantics, while the storage rehearsal proves those semantics can survive the product's real lifetime.
Reviewers should choose structured clone vs JSON by boundary ownership: browser-native graph fidelity on one side, deliberate text interoperability on the other. The runnable matrix supplies evidence for that choice without declaring a universal serializer.