DEV Community

Snappy Tools
Snappy Tools

Posted on • Originally published at snappytools.app

7 Things Most Developers Don't Know About JSON.parse() and JSON.stringify()

JSON.parse() and JSON.stringify() look simple. You call one to get data in, call the other to get it out. But both have second and third arguments that most developers never touch — and those arguments unlock features that would otherwise require custom code.

Here are seven things worth knowing.

1. JSON.stringify() has a space argument

Everyone knows the first argument. Most people have no idea about the third:

const data = { name: "Alice", age: 30, tags: ["admin", "editor"] };

// Default — minified
JSON.stringify(data);
// '{"name":"Alice","age":30,"tags":["admin","editor"]}'

// Indented with 2 spaces
JSON.stringify(data, null, 2);
// {
//   "name": "Alice",
//   "age": 30,
//   "tags": [
//     "admin",
//     "editor"
//   ]
// }

// Indented with a tab character
JSON.stringify(data, null, '\t');
Enter fullscreen mode Exit fullscreen mode

The space argument accepts either a number (number of spaces) or a string (used as the indent). This is the only argument you need to produce human-readable JSON for logging, debugging, or config files.

2. JSON.stringify() has a replacer argument

The second argument is a replacer — a way to transform or filter data during serialisation. It can be an array (allowlist of keys) or a function (transform each value):

const user = { id: 1, name: "Alice", password: "secret", token: "xyz" };

// Array replacer — only include these keys
JSON.stringify(user, ["id", "name"], 2);
// { "id": 1, "name": "Alice" }

// Function replacer — strip null values
JSON.stringify({ a: 1, b: null, c: 3 }, (key, value) => {
  if (value === null) return undefined; // returning undefined removes the key
  return value;
});
// '{"a":1,"c":3}'

// Function replacer — transform dates to ISO strings
JSON.stringify({ createdAt: new Date() }, (key, value) => {
  if (value instanceof Date) return value.toISOString();
  return value;
});
Enter fullscreen mode Exit fullscreen mode

The function replacer receives every key-value pair, including the root object (key is "" at the root). Return undefined to omit the key.

3. JSON.parse() has a reviver argument

The reviver is the parse-time counterpart to replacer:

const json = '{"createdAt":"2024-01-15T10:30:00.000Z","updatedAt":"2024-02-01T08:00:00.000Z"}';

// Without reviver — dates are strings
JSON.parse(json).createdAt;
// "2024-01-15T10:30:00.000Z" (string, not Date)

// With reviver — auto-convert ISO date strings to Date objects
JSON.parse(json, (key, value) => {
  if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
    return new Date(value);
  }
  return value;
});
// { createdAt: Date object, updatedAt: Date object }
Enter fullscreen mode Exit fullscreen mode

The reviver runs bottom-up — leaf nodes are processed before their parents. This matters when you're transforming nested structures.

4. JSON.stringify() silently drops certain values

This one surprises a lot of people:

const obj = {
  a: 1,
  b: undefined,          // omitted
  c: function() {},      // omitted
  d: Symbol("x"),        // omitted
  e: NaN,                // becomes null
  f: Infinity,           // becomes null
  g: -Infinity,          // becomes null
};

JSON.stringify(obj);
// '{"a":1,"e":null,"f":null,"g":null}'
Enter fullscreen mode Exit fullscreen mode

undefined, functions, and Symbols are silently dropped from objects. In arrays, they become null:

JSON.stringify([1, undefined, function(){}, 4]);
// '[1,null,null,4]'
Enter fullscreen mode Exit fullscreen mode

NaN and Infinity become null — a detail that trips up people working with mathematical data or price calculations in JavaScript.

5. Circular references throw — but you can handle them

const obj = { name: "foo" };
obj.self = obj; // circular reference

JSON.stringify(obj); // throws TypeError: Converting circular structure to JSON
Enter fullscreen mode Exit fullscreen mode

To handle circular references, use a replacer with a WeakSet:

function safeStringify(obj, space) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]';
      seen.add(value);
    }
    return value;
  }, space);
}

const obj = { name: "foo" };
obj.self = obj;
safeStringify(obj);
// '{"name":"foo","self":"[Circular]"}'
Enter fullscreen mode Exit fullscreen mode

This is useful for logging objects that might have circular references (DOM nodes, some Node.js objects).

6. toJSON() lets objects control their own serialisation

If an object has a toJSON() method, JSON.stringify() calls it and serialises the return value instead:

class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }

  toJSON() {
    return `${this.amount} ${this.currency}`; // serialise as a string
  }
}

const price = new Money(9.99, "USD");
JSON.stringify({ price }); // '{"price":"9.99 USD"}'
Enter fullscreen mode Exit fullscreen mode

Date objects use this mechanism — Date.prototype.toJSON calls toISOString(), which is why dates serialise to ISO strings automatically. You can use the same pattern for Money, BigInt wrappers, custom types — any class where the default object form isn't the right representation.

7. Deeply nested structures and the performance limit

JSON.parse() and JSON.stringify() are synchronous and run on the main thread. For very large payloads (>1 MB), they can cause UI jank in a browser context. A few strategies:

// For large payloads, consider structuredClone() instead of JSON round-trip for deep cloning
const deep = structuredClone(bigObject); // faster, handles more types, no JSON overhead

// For streaming large JSON (Node.js), use a streaming parser like JSONStream
const JSONStream = require('JSONStream');
fs.createReadStream('large.json').pipe(JSONStream.parse('*')).on('data', row => {
  // process one row at a time without loading the whole file
});

// For the browser, consider JSON.parse on a background worker for >5MB files
const worker = new Worker('json-worker.js');
worker.postMessage(largeJsonString);
worker.onmessage = e => console.log(e.data); // parsed object
Enter fullscreen mode Exit fullscreen mode

JavaScript's V8 engine has an effective nesting limit of around 500 levels deep before a stack overflow. In practice, no real-world JSON structure gets close to this, but if you're building a JSON generator, keep this in mind.


Quick Reference

Argument JSON.stringify(value, replacer, space)
replacer Array of keys to include, or function to transform each value
space Number of spaces or string to use for indentation
Argument JSON.parse(text, reviver)
reviver Function to transform each parsed value (runs bottom-up)

If you need to format, validate, or minify JSON without writing any code, the SnappyTools JSON Formatter does it in one click — paste your JSON, get it formatted, validated, and ready to copy. No setup, no signup, runs entirely in your browser.

Top comments (0)