[]previous and next page

serialize and deserialize

serialize(value) returns JSON text that is safe to put inside <script type="application/json">; deserialize(text) is its inverse.

For agentsUpdated

serialize(value) returns JSON text that is safe to put inside <script type="application/json">; deserialize(text) is its inverse. They are not JSON.stringify: they carry types JSON cannot, keep shared references and cycles, and are safe to read.

Value Round trip
undefined, NaN, Infinity, -Infinity, -0, bigint exact
Date, URL, Map, Set exact
plain objects and arrays, nested to any depth exact; shared references and cycles keep their identity
functions, symbols, class instances serialize throws a TypeError
serialize-demo.tsxtsx
import { deserialize, serialize } from "@shadow-js/web/server";

const shared = { n: 1 };
const input = {
  missing: undefined,
  numbers: [Number.NaN, -0, 10n],
  when: new Date(0),
  tags: new Set(["a"]),
  lookup: new Map([["k", shared]]),
  again: shared,
  html: "</script><!--",
};
const text = serialize(input);
const back = deserialize(text) as typeof input;

console.log(back.missing === undefined && "missing" in back);
console.log(back.numbers.length === 3, back.numbers.length, back.lookup.size, String(back.numbers[0]));
console.log(Object.is(back.numbers[1], -0) && back.again === back.lookup.get("k"));
console.log(text.includes("\\u003c/script\\u003e\\u003c!--"));
console.log(!text.includes("<") && !text.includes(">") && !text.includes("&"));

// A hostile key stays an ordinary own property; the result has a null prototype.
const hostile = deserialize(serialize(JSON.parse('{"__proto__": 1, "list": [1, 2]}'))) as Record<string, unknown>;
console.log("proto-safe:", Object.getPrototypeOf(hostile) === null, Object.keys(hostile).join(","));
Expected output
true
true 3 1 NaN
true
true
true
proto-safe: true __proto__,list

Serialization rules:

  • <, >, &, U+2028 and U+2029 are written as \uXXXX escapes, so no value can close the script element, start a comment or break a JavaScript line. The JSON stays valid.
  • Parsing never evaluates code. Objects are rebuilt with a null prototype and keys are defined as own data properties, so __proto__, constructor and prototype are plain data.
  • Malformed input throws (SyntaxError for non-JSON, TypeError for JSON that is not this format) instead of returning something half-built.
  • Both directions are iterative; nesting depth cannot overflow the call stack.