4.1 KiB
4.1 KiB
json-encode
Purpose
Implements the serialize function that converts any JavaScript value to a tagged JSON string preserving type information beyond what JSON.stringify supports. The encoder traverses the value graph recursively, wraps non-primitive types in { "__t": tag, "v": ... } envelopes handled by the type registry, and detects both cycles and excessive nesting depth before they cause runtime errors.
The tagged JSON format is self-describing: a receiver that imports deserialize from the same package can reconstruct the original value graph without out-of-band schema information.
Features
- Handles all types registered in the global registry (Date, Map, Set, BigInt, TypedArrays, URL, RegExp, custom classes)
- Preserves
undefinedas{"__t":"__undefined","v":null}(normally silently dropped byJSON.stringify) - Preserves
NaNas{"__t":"__NaN","v":null}and±Infinityas{"__t":"__Infinity","v":"+"/"−"} - Detects circular references via
WeakSetand throwsCircularReferenceErrorwith the structural path - Configurable maximum depth (default 100) via
maxDepthoption; throwsMaxDepthExceededError - Configurable maximum byte length via
maxSizeoption; throwsMaxSizeExceededError - Honour
AbortSignalcancellation; checked every 256 nodes for minimal overhead seenset cleared per-branch (re-entrant DAG nodes are allowed, only true cycles throw)
Usage: Basic Example
import { serialize } from "@suaveplan/serialize";
const obj = {
created: new Date("2024-01-01"),
tags: new Set(["a", "b"]),
count: BigInt(9007199254740993),
};
const json = serialize(obj);
// '{"created":{"__t":"__Date","v":"2024-01-01T00:00:00.000Z"},...}'
Usage: Advanced Example
import { serialize } from "@suaveplan/serialize";
const controller = new AbortController();
try {
// Enforce max 50KB output and max 20 levels of nesting
const json = serialize(hugeObject, {
maxDepth: 20,
maxSize: 50_000,
signal: controller.signal,
});
sendToNetwork(json);
} catch (err) {
if (err instanceof MaxSizeExceededError) {
console.error("Payload too large:", err.context?.size, "bytes");
} else if (err instanceof MaxDepthExceededError) {
console.error("Too deeply nested at depth:", err.context?.depth);
}
}
// Cancel mid-way through a large serialization:
controller.abort();
API Reference
serialize(value: unknown, opts?: SerializeOptions): string
Converts value to a tagged JSON string.
Parameters:
value— any JavaScript value; functions and symbols are silently dropped (same asJSON.stringify)opts.maxDepth— maximum recursion depth beforeMaxDepthExceededErroris thrown (default:100)opts.maxSize— maximum byte length of the resulting JSON string; checked after encoding (default: unlimited)opts.signal— anAbortSignal; checked every 256 nodes; throws the signal's reason if aborted
Returns: a JSON string in the tagged wire format.
Throws:
CircularReferenceErrorwhen a cycle is detectedMaxDepthExceededErrorwhen nesting exceedsmaxDepthMaxSizeExceededErrorwhen output exceedsmaxSizebytes- The
AbortSignal's reason whensignalis aborted
SerializeOptions
interface SerializeOptions {
maxDepth?: number; // default: 100
maxSize?: number; // default: unlimited
signal?: AbortSignal;
}
Implementation Notes
- The cycle-detection
WeakSet(seen) is managed with add-before/delete-after so that DAG structures (same object reachable via multiple paths) do not falsely trigger the cycle check. Only true back-edges to an ancestor in the current call stack throw. - Signal checking happens every 256 nodes rather than every node to keep the hot path free of extra branch overhead.
- The
maxSizecheck occurs afterJSON.stringifyon the fully encoded intermediate object, not during encoding. This trades one additional pass for implementation simplicity and avoids double-counting tag overhead. undefinedat object property positions is omitted byJSON.stringifybut handled explicitly here by converting it to a tagged sentinel before theJSON.stringifycall.