Files
2026-05-27 23:47:53 +01:00

4.1 KiB
Raw Permalink Blame History

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 undefined as {"__t":"__undefined","v":null} (normally silently dropped by JSON.stringify)
  • Preserves NaN as {"__t":"__NaN","v":null} and ±Infinity as {"__t":"__Infinity","v":"+"/"−"}
  • Detects circular references via WeakSet and throws CircularReferenceError with the structural path
  • Configurable maximum depth (default 100) via maxDepth option; throws MaxDepthExceededError
  • Configurable maximum byte length via maxSize option; throws MaxSizeExceededError
  • Honour AbortSignal cancellation; checked every 256 nodes for minimal overhead
  • seen set 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 as JSON.stringify)
  • opts.maxDepth — maximum recursion depth before MaxDepthExceededError is thrown (default: 100)
  • opts.maxSize — maximum byte length of the resulting JSON string; checked after encoding (default: unlimited)
  • opts.signal — an AbortSignal; checked every 256 nodes; throws the signal's reason if aborted

Returns: a JSON string in the tagged wire format.

Throws:

  • CircularReferenceError when a cycle is detected
  • MaxDepthExceededError when nesting exceeds maxDepth
  • MaxSizeExceededError when output exceeds maxSize bytes
  • The AbortSignal's reason when signal is 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 maxSize check occurs after JSON.stringify on the fully encoded intermediate object, not during encoding. This trades one additional pass for implementation simplicity and avoids double-counting tag overhead.
  • undefined at object property positions is omitted by JSON.stringify but handled explicitly here by converting it to a tagged sentinel before the JSON.stringify call.