serialize and deserialize
serialize(value) returns JSON text that is safe to put inside <script type="application/json">; deserialize(text) is its inverse.
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(","));
true
true 3 1 NaN
true
true
true
proto-safe: true __proto__,listSerialization rules:
<,>,&, U+2028 and U+2029 are written as\uXXXXescapes, 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__,constructorandprototypeare plain data. - Malformed input throws (
SyntaxErrorfor non-JSON,TypeErrorfor JSON that is not this format) instead of returning something half-built. - Both directions are iterative; nesting depth cannot overflow the call stack.