Lumenize Structured Clone
Zero-dependency serialization with full-fidelity type support for complex JavaScript types including cycles, Errors, Request/Response, special numbers, and more.
Why This Package?
When building RPC systems, storing data, or moving data over the network, you need to serialize complex JavaScript objects. JSON alone isn't enough:
- Lost Types: JSON loses
Date,Map,Set,Errorobjects - No Circular or Aliased References: JSON can't handle cyclic or aliasesed structure
- No Special Numbers: JSON converts
NaNandInfinitytonull - No Web API Objects: JSON can't serialize
Request,Response,Headers,URL
This package solves all of these problems.
Installation
npm install @lumenize/structured-clone
Supported Types
While this package was originally optimized for the Cloudflare edge compute platform, nothing in it is Cloudflare-specific. All tests run in three environments: Node.js, headless Chromium browser, and simulated Cloudflare Workers. Our primary use case includes browser clients, and the full test suite passes in all environments.
Not a Cloudflare user? Feel free to ignore the first three columns in the comparison table below. This package provides comprehensive structured cloning with better Error handling, special number support, and Web API object serialization—useful in any JavaScript environment.
| Type | Workers RPC | DO Storage | Cap'n Web | Lumenize | Notes |
|---|---|---|---|---|---|
| Cycles & Aliases | ✅→❌ | ✅→❌ | ❌ | ✅ | Cap'n Web throws error Cloudflare plans to remove |
| Primitives | |||||
| undefined | ✅ | ✅ | ✅ | ✅ | |
| null | ✅ | ✅ | ✅ | ✅ | |
| Special Numbers | |||||
| NaN | ✅ | ✅ | ❌→✅ | ✅ | Cap'n Web returns null |
| Infinity | ✅ | ✅ | ❌→✅ | ✅ | Cap'n Web returns null |
| -Infinity | ✅ | ✅ | ❌→✅ | ✅ | Cap'n Web returns null |
| Built-in Types | |||||
| BigInt | ✅ | ✅ | ✅ | ✅ | |
| Date | ✅ | ✅ | ✅ | ✅ | |
| RegExp | ✅ | ✅ | ❌→✅ | ✅ | |
| Map | ✅ | ✅ | ❌→✅ | ✅ | |
| Set | ✅ | ✅ | ❌→✅ | ✅ | |
| ArrayBuffer | ✅ | ✅ | ❌→✅ | ✅ | |
| Uint8Array | ✅ | ✅ | ✅ | ✅ | |
| Errors | |||||
| Error (thrown) | ✅ | N/A | ⚠️ | ✅ | Cap'n Web loses name and remote stack |
| Error (value) | ✅ | ⚠️ | ⚠️→✅ | ✅ | Cap'n Web loses name and remote stack |
| Web API Types | |||||
| Request | ✅ | ❌ | ❌ | ✅ | |
| Response | ✅ | ❌ | ❌ | ✅ | |
| Headers | ✅ | ✅ | ❌→✅ | ✅ | Cap'n Web: type annotation only, no serialization |
| URL | ❌ | ❌ | ❌→✅ | ✅ | |
| Streams | |||||
| ReadableStream | ✅ | ❌ | ❌ | ❌ | Cap'n Web: "may be added" |
| WritableStream | ✅ | ❌ | ❌ | ❌ | Lumenize: "just use WebSockets" |
Note, we use ❌→✅ or ⚠️→✅ in the Cap'n Web column for certain rows because we have submitted a pull request to upgrade Cap'n Web's type support.
Key Features
- Platform Independent: Works in Node.js, browsers, Deno, Bun, and Cloudflare Workers
- Full Type Support: All primitives, special numbers (
NaN,Infinity,-Infinity), built-in types (Date,RegExp,Map,Set), typed arrays, and Web API objects - Error Fidelity: Preserves stack traces,
causechains, custom properties, and subclass types (including custom Error classes!) - Circular References: Automatically handled with
$lmzreference markers - Zero Dependencies: No external packages required
- Human-Readable Format: Tuple-based
["type", data]format for debugging - Async API: Supports Request/Response body reading
Basic Usage
The core API is simple: stringify() to serialize, parse() to deserialize.
Note: Both functions are async to support Request/Response body reading.
import { stringify, parse } from '@lumenize/structured-clone';
const user = {
name: 'Alice',
age: 30,
active: true
};
const serialized = await stringify(user);
const restored = await parse(serialized);
expect(restored).toEqual(user);
This works with complex nested structures, arrays, and all types shown in the table above.
Key Features with Examples
Error Chains with cause
Error chaining is fully preserved, including nested causes:
const networkError = new Error('Connection timeout');
const appError = new Error('Failed to fetch user data', {
cause: networkError
});
const restored = await parse(await stringify(appError));
expect(restored.message).toBe('Failed to fetch user data');
expect(restored.cause).toBeInstanceOf(Error);
expect(restored.cause.message).toBe('Connection timeout');
Custom Error Properties
Add arbitrary properties to Errors—they're all preserved:
const apiError: any = new Error('API request failed');
apiError.statusCode = 500;
apiError.endpoint = '/api/users';
const restored: any = await parse(await stringify(apiError));
expect(restored.message).toBe('API request failed');
expect(restored.statusCode).toBe(500);
Error Subclass Preservation
All standard JavaScript Error types are automatically preserved with correct instanceof behavior:
Standard Error Types (Built-in):
Error- Base error typeTypeError- Type-related errorsRangeError- Value out of rangeReferenceError- Invalid referencesSyntaxError- Syntax parsing errorsURIError- URI encoding/decoding errorsEvalError- Eval-related errorsAggregateError- Multiple errors combined
Example:
const typeError = new TypeError('Expected string, got number');
const restored = await parse(await stringify(typeError));
expect(restored).toBeInstanceOf(TypeError); // ✅ Type preserved!
expect(restored).toBeInstanceOf(Error); // ✅ Also an Error
expect(restored.name).toBe('TypeError');
expect(restored.message).toBe('Expected string, got number');
How it works:
We use dynamic constructor lookup via globalThis[error.name]:
- Error is serialized with its
nameproperty (e.g.,"TypeError") - During deserialization, we look up
globalThis["TypeError"] - If found, we create a new instance of that constructor
- If not found, we fall back to base
Error(but preserve thenameproperty)
Custom Error Classes:
Custom Error classes work too, but require explicit global registration since module imports are not in global scope:
// Your custom Error class (in a module)
export class ValidationError extends Error {
name = 'ValidationError';
constructor(message: string, public field: string) {
super(message);
}
}
To enable type preservation, register it globally:
// Register on globalThis so deserializer can find it
(globalThis as any).ValidationError = ValidationError;
Now serialization preserves the type:
const error = new ValidationError('Invalid email', 'email');
const restored = await parse(await stringify(error));
// ...
expect(restored instanceof ValidationError).toBe(true); // ✅ true!
expect((restored as any).field).toBe('email'); // ✅ 'email' (custom property preserved)
expect(restored.name).toBe('ValidationError'); // ✅ 'ValidationError'
Without global registration:
If you don't register the class globally, deserialization falls back to base Error but still preserves custom properties and the name:
// No global registration
const error = new ValidationError('Invalid email', 'email');
const restored = await parse(await stringify(error));
expect(restored instanceof ValidationError).toBe(false); // ❌ false (no type)
expect(restored instanceof Error).toBe(true); // ✅ true (fallback)
expect((restored as any).field).toBe('email'); // ✅ 'email' (property still preserved!)
expect(restored.name).toBe('ValidationError'); // ✅ 'ValidationError' (name preserved)
Best Practice: Register custom Error classes in your app's initialization code, before any deserialization happens. This ensures full type preservation across serialization boundaries.
Web API Objects Example
Serialize Request/Response objects for storage, queues, or RPC:
const apiData = {
incomingRequest: new Request('https://api.example.com/endpoint', {
method: 'POST',
body: 'request data'
}),
outgoingResponse: new Response('response data', {
status: 200
}),
timestamp: Date.now()
};
const restored = await parse(await stringify(apiData));
expect(restored.incomingRequest).toBeInstanceOf(Request);
// ...
Note: Request/Response bodies are consumed during serialization (streams become strings). You should clone the Request/Response if you need them preserved.
Advanced API
The package provides a three-tier API for different use cases:
Tier 1: stringify() / parse() - JSON String I/O
The main API for most use cases. Serializes to/from JSON strings:
const jsonString = await stringify(complexObject); // Returns JSON string
const restored = await parse(jsonString); // Reconstructs from JSON string
Use for: HTTP requests/responses, file storage, logging
Tier 2: preprocess() / postprocess() - Intermediate Format
For scenarios where you want the structured object format without JSON stringification:
const intermediate = await preprocess(complexObject); // Returns { root, objects }
const restored = await postprocess(intermediate); // Reconstructs from object
Use for: MessagePort, BroadcastChannel, IndexedDB, or when you need custom JSON handling
Tier 3: encode*() / decode*() - Type-Specific Encoding
For encoding specific Web API types when you need explicit control:
// Encode Request for storage or transmission
const encoded = await encodeRequest(request); // Plain object
// Later... decode back to Request
const decoded = decodeRequest(encoded); // Reconstructed Request
Use for: Embedding Request/Response in larger structures that are separately serialized (e.g., Durable Object storage, Cloudflare Queues)
Example - DO Storage:
// Store Request in DO storage
const encodedRequest = await encodeRequest(request);
ctx.storage.kv.put('my-request', {
timestamp: Date.now(),
request: encodedRequest // Plain object, storage handles serialization
});
// Later... retrieve and decode
const stored = ctx.storage.kv.get('my-request');
const restoredRequest = decodeRequest(stored.request);
Performance
The tuple-based $lmz format is designed for efficiency:
- Fast serialization - optimized two-pass algorithm with cycle detection
- Fast parsing - efficient reconstruction of complex structures
- Human-readable format for debugging:
["type", data]with["$lmz", ref]for cycles/aliases
Limitations
- Symbols: Cannot be serialized (throws
TypeError) - Functions: Converted to internal markers (preserved structure, not executable)
- Signed Zero:
-0becomes+0(What is -0 anyway?) - Streams:
ReadableStream/WritableStreamnot supported
License
MIT License © 2025 Larry Maccherone
Attribution
Inspired by:
- Cap'n Web (tuple format) - https://github.com/cloudflare/capnweb
- @ungap/structured-clone (cycle detection) - https://github.com/ungap/structured-clone by Andrea Giammarchi
See ATTRIBUTIONS.md for full attribution details.