JSONBeautify JSON Linter

JSON in JavaScript, 2024–2026: What Actually Changed in JSON.parse

JSONBeautify — Free Online JSON Formatter, Validator & Minifier Guides · Updated 2026-10-01 · All guides

Every claim on this page was run on this machine: Node v24.21.0, V8 13.6.233.17-node.53. Outputs are pasted, not remembered. Where a proposal has not shipped, that is stated rather than hinted at.

The reviver is not what people think it is

JSON.parse(text, reviver) calls your function bottom-up, leaves first, with (key, value) — and the value has already been coerced by the parser. Watch the order on a nested document:

const order = [];
JSON.parse('{"a":{"b":[1,2]},"c":3}', (k, v) => { order.push(`${k}:${JSON.stringify(v)}`); return v; });
// 0:1 | 1:2 | b:[1,2] | a:{"b":[1,2]} | c:3 | :{"a":{"b":[1,2]},"c":3}

Array indexes arrive as string keys "0", "1". The final call gets key "" and the completed root object. Returning undefined deletes the key — that is the documented removal mechanism, not a bug:

JSON.parse('{"a":1,"b":2}', (k, v) => (k === "a" ? undefined : v));  // {"b":2}

Two pitfalls follow directly from bottom-up evaluation with already-lossy values.

Dates. JSON has no date type, so Date becomes an ISO string via the toJSON hook and never comes back as a Date:

JSON.stringify(new Date("2026-10-01T15:30:00Z"));   // "2026-10-01T15:30:00.000Z"
JSON.parse('{"d":"2026-10-01T15:30:00Z"}').d instanceof Date;   // false

A reviver that matches an ISO pattern and constructs Date objects fixes the common case:

const ISO = /^\d{4}-\d{2}-\d{2}T/;
JSON.parse(raw, (k, v) => (typeof v === "string" && ISO.test(v) ? new Date(v) : v));

Honest caveat: that is a heuristic. A field named notes containing an ISO-looking string becomes a Date, and the reviver cannot tell you which fields were strings in the source — no type tag, no context. If dates matter, put an explicit "__type":"date" envelope on them.

Big integers. By the time your reviver sees a number, precision is already gone. JSON.parse throws away digits before you get a vote:

JSON.parse('{"n":9007199254740993}').n;   // 9007199254740992

A reviver checking typeof v === "string" for big numbers does nothing, because the parser hands you a number, not the raw text:

// Broken pattern that circulates widely:
JSON.parse('{"n":12345678901234567890123}', (k, v) =>
  typeof v === "string" ? BigInt(v) : v);
// → { n: 1.2345678901234568e+22 }   still a number, still wrong

What actually shipped: source text access

The TC39 proposal proposal-json-parse-with-source reached Stage 4 and shipped behind the shipped name: the reviver now takes a third argument carrying the raw source text for primitive values. On Node 24 it works:

const BIG = /^-?\d{16,}$/;
const revive = (k, v, ctx) =>
  typeof v === "number" && ctx?.source && BIG.test(ctx.source) ? BigInt(ctx.source) : v;
const o = JSON.parse('{"id":12345678901234567890123,"small":42}', revive);
typeof o.id;      // "bigint"
String(o.id);     // "12345678901234567890123"
typeof o.small;   // "number"

The source is exact — escapes and exponent notation preserved (1e3 arrives as "1e3", "a\u0062" as the literal backslash text). It is supplied for primitives only; objects and arrays get undefined, and the root call gets undefined. That is the whole feature, and it is the fix for the big-integer problem above.

Companion piece: JSON.rawJSON(text) and JSON.isRawJSON(value) are present on this build. One sharp edge — JSON.rawJSON only accepts primitive JSON text. Objects throw:

JSON.stringify(JSON.rawJSON('12345678901234567890123'));  // '12345678901234567890123'  ✔
JSON.stringify(JSON.rawJSON('{"big": 12345678901234567890123}'));  // SyntaxError  ✗

So rawJSON is for splicing a vetted scalar into a larger document without re-serialising it; it is not a lossless-object wrapper. Frozen objects, isRawJSON returns true, and a plain object returns false.

Honest caveat on availability: this is a V8/Node 24 story. Browser support is uneven, so feature-detect before relying on it and keep the string-quoting fallback below for code that must run everywhere.

The supersets never came, and one still won't arrive

JSON.parse still rejects everything the JS-family dialects allow. Verified rejects on Node 24:

{"a":1,}        trailing comma        REJECT
{a:1}           naked key             REJECT
//x\n{"a":1}    leading comment       REJECT
{"a":1} //x     trailing comment      REJECT
`t`             template literal      REJECT
{"a":.5}        bare-leading-dot      REJECT
{"a":+1}        explicit plus         REJECT

There is no JSON.supersetParse on JSON's own properties. The JSON superset proposal (TC39 stage 1, proposal-json-superset) has not progressed to a shipped feature as of this writing — it remains an idea with a README. JSON5 and JSONC stay where they belong: editor and config files your toolchain parses deliberately, never an API contract. When a human pastes a commented config at you, run it through Auto-Repair and read what it changed.

1e999 → Infinity, but stringify(Infinity) → null

The asymmetry catches everyone. JSON grammar permits an unbounded number literal, so the parser evaluates 1e999 and IEEE-754 saturates it:

JSON.parse('1e999');                    // Infinity
JSON.parse('[1e999,-1e999]');           // [Infinity, -Infinity]
JSON.stringify(Infinity);               // null
JSON.stringify({ a: Infinity });        // {"a":null}
JSON.stringify([1, Infinity, 3]);       // [1,null,3]

Not a bug — JSON has no Infinity literal, and undefined/functions/Infinity/NaN all serialise to null inside objects and arrays. The damage: the round trip is lossy and silent. If a field can overflow, reject it at the boundary:

const strict = (v) => { if (typeof v === "number" && !Number.isFinite(v)) throw new TypeError("non-finite number"); return v; };
JSON.parse(raw, (k, v) => strict(v));

BigInt without tears

JSON.stringify(1n) throws TypeError: Do not know how to serialize a BigInt. Three patterns that work, in order of preference:

  1. Strings on the wire, always. "id": "12345678901234567890123". Every consumer, every language, no precision question. This is the right answer and the other two are compromises.
  2. Reviver round trip with source access (Node 24, shown above) plus a replacer that tags the type: String(v) + "n" on the way out, regex-strip on the way in.
  3. Quote-by-regex before parsing for runtimes without source access — crude, and it must run before JSON.parse, never after:
JSON.parse(text.replace(/:\s*(\d{16,})(?=[,\s}\]])/g, ': "$1"'));   // id arrives as string

structuredClone vs the JSON round trip

structuredClone is available in Node ≥17 and every current browser, and it is strictly better for state snapshots:

const s = { d: new Date("2026-10-01T00:00:00Z"), m: new Map([["k",1]]), u: undefined };
const rt    = JSON.parse(JSON.stringify(s));
const cl    = structuredClone(s);
rt.d instanceof Date;      // false   (string)
cl.d instanceof Date;      // true
"u" in rt;                 // false   (undefined dropped)
"u" in cl;                 // true
cl.m.get("k");             // 1       (Map survives)

JSON round-trip loses Date, Map, Set, RegExp, typed arrays' prototypes, -0, undefined keys, and turns sparse-ish holes into null. structuredClone preserves Date, Map, Set, ArrayBuffer, Blob, File, Error, and cyclic references; it throws DataCloneError on functions, Symbols, and DOM nodes. Use structuredClone for worker handoff and in-memory state; use JSON only when bytes must cross a boundary, because that is the only reason it exists.

FAQ

Should I upgrade to Node 24 purely for reviver source access? No. It is a nice fix for big-integers-on-the-wire, but the correct architecture is still strings for IDs. Feature-detect it; do not assume it.

Why does my toJSON method get called? JSON.stringify checks for toJSON on every value before serialising — the same hook Date uses. Any object can define it; your ISO strings may be coming from something other than a Date.

Why does __proto__ in a payload not poison my object? JSON.parse creates it as an ordinary own property, so JSON.parse('{"__proto__":{"evil":true}}').evil is undefined. That is parse-time only — a merge helper or Object.assign onto an existing target can still walk it. See patterns and gotchas.

What changed for this website? Nothing user-visible: this tool is JSON.parse plus JSON.stringify in your browser, so you get whatever the engine provides. Feature-detect before depending on it. The format buttons remain 2-space and 4-space only.

Developer Sponsor / Partner
Copied to clipboard!