JSON Formatter & Validator

JSON.parse Performance: How V8 Handles Large Payloads

Modern V8 (the engine behind Node.js, Chrome, Deno) does not use a generic recursive-descent parser for JSON.parse. Since 2019 V8 ships a dedicated, fast-path parser written in C++ that operates on UTF-16 strings directly, allocates objects in the young generation, and uses transition trees (hidden classes) to share shape information across sibling objects in an array. For a payload like [{"id":1,"name":"a"},{"id":2,"name":"b"}, ...], every object after the first is created with the same hidden class — so property lookups later are monomorphic and inlined. The cost of JSON.parse is dominated by three things: (1) tokenization and unescaping of strings, (2) GC pressure from the resulting object graph, and (3) the cost of any reviver callback. Adding a reviver function is the single biggest performance footgun: it forces V8 onto a slow generic path and invokes a JS function for every key in the tree. On a 10 MB payload the difference is typically 4–8x. If you only need to transform a handful of fields, parse first and walk the result yourself. For very large arrays of homogeneous objects, V8 will often beat a streaming JSON library because the fast path is so well-optimized. But once an individual document exceeds ~100 MB, JSON.parse holds the entire raw string AND the resulting object tree in memory at once, so peak RSS can be 3–4x the file size. At that point switch to a streaming parser like simdjson, oboe.js, or stream-json, which emits events as tokens are produced and never materializes the full tree. V8 also caches the parser's tokenization buffer between calls, so a tight loop that calls JSON.parse on many small payloads is faster than the same total bytes parsed in one giant call. If you control the wire format, prefer NDJSON (one JSON object per line) — it parses incrementally, recovers from per-line errors, and works with simple line-buffered IO.
// SLOW: reviver kills the fast path
const data = JSON.parse(raw, (key, val) => {
  if (key === 'createdAt') return new Date(val);
  return val;
});

// FAST: parse first, walk later
const data = JSON.parse(raw);
function walk(o) {
  for (const k in o) {
    if (k === 'createdAt' && typeof o[k] === 'string') o[k] = new Date(o[k]);
    else if (o[k] && typeof o[k] === 'object') walk(o[k]);
  }
}
walk(data);

// FASTER for huge files: NDJSON
import readline from 'node:readline';
const rl = readline.createInterface({ input: fs.createReadStream('big.ndjson') });
for await (const line of rl) {
  const row = JSON.parse(line); // 1 record at a time
  process(row);
}

Common JSON Pitfalls You Will Hit in Production

JSON looks simple and that is exactly why it bites. RFC 8259 defines the grammar tightly: numbers are IEEE-754 doubles only, the only literals are true / false / null, strings must be double-quoted UTF-8, and there is no native date, no comments, no trailing commas, and no Infinity / NaN. Every "weird production bug" with JSON is one of these six rules getting violated by reality. NaN and Infinity are not valid JSON values. JavaScript's JSON.stringify silently converts them to null, which means a metric like { "p99": Infinity } gets serialized as { "p99": null } and your dashboard quietly displays zero. The fix is to clamp or special-case these values before serializing — never trust the round-trip. Date objects round-trip asymmetrically. JSON.stringify(new Date()) emits an ISO-8601 string via the Date.prototype.toJSON method, but JSON.parse has no idea that string is a date — you get back a string, not a Date. If your code does data.createdAt.getTime() after a network round-trip, it will throw. Either wrap parses with a known schema (zod, valibot, ajv) or document explicitly that timestamps come back as strings. BigInt is a hard error: JSON.stringify(1n) throws TypeError: Do not know how to serialize a BigInt. Database drivers that return 64-bit IDs as BigInt (Postgres bigint, MySQL UNSIGNED BIGINT) hit this constantly. The pragmatic fixes are (a) define BigInt.prototype.toJSON = function() { return this.toString() } at app boot, accepting that the receiver will get a string, or (b) use a serializer like superjson that preserves the type. Circular references are a TypeError too. Anything from a DOM tree to a graph data structure with parent pointers will explode JSON.stringify. The replacer parameter lets you skip known cycle keys (return undefined to drop). For debugging, util.inspect or structuredClone (which detects cycles internally) are friendlier. Finally, key ordering: the spec says objects are unordered, but JSON.stringify in V8 emits keys in insertion order with integer-like keys first. If your tests assert exact JSON strings, you are testing implementation detail — assert against a parsed object instead.
// NaN/Infinity silently become null
JSON.stringify({ p99: NaN, max: Infinity });
// => '{"p99":null,"max":null}'  (BUG: silent data loss)

// Date asymmetric round-trip
const o = { at: new Date() };
const back = JSON.parse(JSON.stringify(o));
back.at.getTime(); // TypeError: back.at.getTime is not a function

// BigInt fix at app boot
BigInt.prototype.toJSON = function () { return this.toString(); };

// Circular reference handler
function safeStringify(obj) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (k, v) => {
    if (typeof v === 'object' && v !== null) {
      if (seen.has(v)) return '[Circular]';
      seen.add(v);
    }
    return v;
  });
}

JSON vs JSON5 vs JSONC — Pick the Right Dialect

JSON itself is frozen — RFC 8259 is the spec and it deliberately omits anything human-friendly. That austerity is great for wire formats and terrible for hand-edited config files, which is why two unofficial dialects dominate the config space. JSONC ("JSON with comments") is what VS Code, TypeScript's tsconfig.json, and most Microsoft tooling use. It adds exactly two things to RFC 8259: line comments (//) and block comments (/* */). Trailing commas are tolerated by the VS Code parser but are not part of any written spec. JSONC is intentionally minimal — every JSONC document is a JSON document with extra whitespace, modulo the comments. There is no library you need to install in TypeScript projects; the compiler ships its own tolerant parser, and stripping comments with a small regex (with care for strings) gives you valid JSON. JSON5 is more ambitious: it adds single-quoted strings, unquoted ECMAScript-5 identifier keys, multi-line strings, hexadecimal numbers, leading/trailing decimal points (.5, 5.), positive sign on numbers, NaN and Infinity, and trailing commas. The trade-off is that JSON5 is no longer a subset of JSON — you cannot feed a JSON5 document to JSON.parse. Use it for config files where humans win and machines can use the json5 npm package; never use it as a wire format. A third contender, HJSON, drops quotes from keys and even from short string values. It is even more permissive than JSON5 but has narrower ecosystem support. Avoid it unless your tool already standardizes on it. The rule of thumb: APIs and inter-process messages stay on RFC-8259 strict JSON for maximum compatibility (every language has a stable parser). Editor and build configs that humans actually read and edit can use JSONC for low-friction comments. Reach for JSON5 only when you need its richer features and you control both writer and reader. And never let a JSON5 document leak into a system that expects JSON — the mismatch produces opaque "Unexpected token" errors at the worst possible moment.
// JSON (RFC 8259) — strict
{
  "name": "app",
  "ports": [3000, 3001]
}

// JSONC — comments + (de facto) trailing commas
{
  // tsconfig.json style
  "compilerOptions": {
    "target": "ES2022", /* matches Node 18+ */
    "strict": true,
  },
}

// JSON5 — humans first
{
  name: 'app',          // unquoted key, single-quoted string
  port: 0xABCD,         // hex
  rate: .5,             // leading decimal
  retries: +Infinity,   // signed Infinity
  // multi-line string
  banner: "line1\
line2",
}
Last updated:

About this tool

A JSON formatter parses raw JSON and re-emits it with consistent indentation, sorted keys (optionally), and clear error messages when the input is invalid. It is the fastest way to read a minified API response, hunt down a missing comma, or share a clean payload in a bug report. JSON is the lingua franca of REST and config files, so a fast formatter is part of every developer’s daily kit.

How to use

  1. Paste your JSON into the input panel.
  2. The formatted result appears on the right with proper indentation.
  3. If parsing fails, an error message points to the line and column of the first problem.
  4. Use the Minify button to remove whitespace for production payloads.
  5. Click Copy to put the result on your clipboard for use elsewhere.

Common use cases

  • Reading a minified webhook payload from Stripe, GitHub, or Slack.
  • Pretty-printing a curl response before sharing it in a Slack thread.
  • Validating that a hand-edited package.json or tsconfig.json still parses.
  • Auditing the structure of an OpenAPI / Swagger document.
  • Comparing two JSON payloads after pretty-printing both consistently.
  • Minifying a large config file before embedding it in an environment variable.

Frequently asked questions

Q. Can I use comments in JSON?

A. Standard JSON does not allow comments. Use JSONC (VS Code config), JSON5, or a YAML alternative if you need comments. For pure JSON, strip comments before parsing.

Q. Why does my parser say "Unexpected token"?

A. Almost always a missing/extra comma, an unquoted key, or a single-quoted string. Double quotes are required for both keys and string values.

Q. Are trailing commas allowed?

A. Not in standard JSON. Some parsers (JSON5, JavaScript object literals) allow them, but JSON.parse and most APIs reject them.

Q. How do I represent a date?

A. JSON has no native date type. Use ISO-8601 strings (e.g., 2026-05-01T10:00:00Z) — that is the de-facto convention.