JavaScript JSON

JSON (JavaScript Object Notation) is a lightweight, text-based format for representing structured data. It grew out of JavaScript’s own object-literal syntax, but today it is a language-independent standard used everywhere: REST APIs, configuration files, browser storage, and message queues all speak JSON. In JavaScript, the built-in JSON object gives you two core methods — JSON.stringify() and JSON.parse() — that convert between live JavaScript values and JSON text. Understanding exactly what JSON can and cannot represent, and how the conversion actually works, will save you from some of the most common bugs in web development.

Overview / How it works

JSON looks a lot like a JavaScript object or array literal, but it is stricter. JSON text has its own grammar, independent of JavaScript: object keys must be double-quoted strings, string values must use double quotes (not single quotes), there are no trailing commas, no comments, and only a fixed set of value types is allowed — strings, numbers, booleans, null, objects, and arrays. Anything outside that set (functions, undefined, Symbol, BigInt, most class instances) has no direct JSON representation.

The JSON object bridges this gap in two directions. JSON.stringify(value) walks the value’s structure depth-first and produces a JSON-formatted string. For each value it encounters, it first checks whether the value has a toJSON() method (dates do, for example) and calls that if present. It then converts primitives directly, recurses into arrays and plain objects, and silently drops or nulls out anything JSON cannot represent. JSON.parse(text) does the reverse: it tokenizes and parses the text according to the strict JSON grammar and builds a fresh tree of plain JavaScript objects, arrays, and primitives. If the text does not match the grammar exactly, parsing fails immediately with a SyntaxError — JSON.parse never tries to be lenient or guess what you meant.

A crucial point beginners miss: JSON.parse always creates brand-new objects. It does not restore class instances, methods, prototypes, or references you had before serialization — you get back plain data, and if you need special types (like Date) back, you must reconstruct them yourself.

Syntax

JSON.stringify(value, replacer, space)
JSON.parse(text, reviver)
  • value — the JavaScript value to convert to a JSON string.
  • replacer (optional) — either an array of allowed key names, or a function (key, value) => newValue called for every key/value pair; returning undefined omits that key.
  • space (optional) — a number (spaces per indent level, up to 10) or a string used for pretty-printing; omit it for compact output.
  • text — the JSON-formatted string to parse.
  • reviver (optional) — a function (key, value) => newValue called for every key/value pair after parsing, from the innermost values outward; returning undefined deletes that key.

Examples

Example 1: the basics — converting an object to a JSON string and back.

const user = {
  name: "Ava",
  age: 28,
  isAdmin: false
};

const jsonString = JSON.stringify(user);
console.log(jsonString);
console.log(typeof jsonString);

const parsedBack = JSON.parse(jsonString);
console.log(parsedBack);
console.log(parsedBack.name);
Output:
{"name":"Ava","age":28,"isAdmin":false}
string
{ name: 'Ava', age: 28, isAdmin: false }
Ava

Notice that jsonString is a plain string — a common beginner mistake is treating it like an object right after calling stringify. You must call JSON.parse() to turn it back into a usable JavaScript value, which produces an entirely new object unrelated to the original.

Example 2: pretty-printing with the space argument, and seeing which properties get dropped.

const product = {
  id: 101,
  title: "Wireless Mouse",
  price: 29.99,
  tags: ["electronics", "accessories"],
  inStock: true,
  restock: undefined,
  logInfo: function () {
    console.log("logging");
  }
};

const pretty = JSON.stringify(product, null, 2);
console.log(pretty);
Output:
{
  "id": 101,
  "title": "Wireless Mouse",
  "price": 29.99,
  "tags": [
    "electronics",
    "accessories"
  ],
  "inStock": true
}

The restock property (value undefined) and the logInfo method are both silently omitted from the output, because neither undefined nor functions have a JSON representation. Passing 2 as the third argument tells stringify to indent nested levels with two spaces, which is invaluable for logging and human-readable config files.

Example 3: using a replacer to filter sensitive data, and a reviver to restore a Date.

const event = {
  title: "Team Meeting",
  date: new Date("2026-07-18T10:00:00.000Z"),
  password: "shouldNotBeSaved",
  attendees: 12
};

const filtered = JSON.stringify(event, (key, value) => {
  if (key === "password") return undefined;
  return value;
});

console.log(filtered);

const revived = JSON.parse(filtered, (key, value) => {
  if (key === "date") return new Date(value);
  return value;
});

console.log(revived.date instanceof Date);
console.log(revived.date.getFullYear());
console.log(revived.title);
Output:
{"title":"Team Meeting","date":"2026-07-18T10:00:00.000Z","attendees":12}
true
2026
Team Meeting

Two things happen here. First, Date objects already define a toJSON() method that returns an ISO string, which is why date becomes a plain string automatically — no replacer logic was needed for that. Second, the replacer function strips out password before serialization ever happens, which is a common technique for excluding secrets or internal fields when sending data to a client. The reviver then converts the ISO string back into a real Date instance during parsing.

Under the hood

It helps to see the exact order of operations JSON.stringify performs:

  • It wraps the top-level value in an internal holder object under the key "", so even the root value can be transformed by a replacer or have a toJSON() method.
  • For each value, if it has a callable toJSON() method, that method is called first and its return value is used instead of the original.
  • If a replacer function was passed, it is called next with (key, value), and its return value replaces the value going forward.
  • The (possibly transformed) value is then serialized according to its type: primitives are written directly, arrays are serialized element by element (with undefined, functions, and symbols inside arrays converted to null rather than omitted), and plain objects are serialized property by property in the same order Object.keys() would return, skipping any property whose final value is undefined, a function, or a symbol.
  • The space argument only affects whitespace/formatting — it never changes which data is included.

JSON.parse works in the opposite direction: it first fully parses the text into a plain object/array/primitive tree, validating the grammar as it goes. Only after the whole tree exists does it apply the reviver, walking the tree bottom-up (children are revived before their parents) and calling (key, value) for every property, including the synthetic root. If a reviver call returns undefined, that property is deleted from the parent; otherwise the returned value replaces it. This bottom-up order is why the example above could safely convert date to a real Date object — by the time the parent object’s own reviver call runs, its children have already been finalized.

Common Mistakes

Mistake 1: stringifying an object with a circular reference. JSON.stringify cannot serialize a structure that references itself, because that would produce infinite output.

const node = { name: "root" };
node.self = node;

try {
  JSON.stringify(node);
} catch (error) {
  console.log(error.name);
  console.log(error.message.includes("circular"));
}
Output:
TypeError
true

To fix this, remove the circular link before serializing, use a replacer that tracks and skips already-visited objects, or use a dedicated library. As of newer JavaScript engines, structuredClone() can also handle circular structures, but plain JSON.stringify cannot.

Mistake 2: parsing text that isn’t valid JSON. JSON requires double-quoted keys and string values — a JavaScript object literal written with single quotes or unquoted keys is not valid JSON and will throw.

const invalidJsonString = "{name: 'Ava'}";

try {
  JSON.parse(invalidJsonString);
} catch (error) {
  console.log(error.name);
}
Output:
SyntaxError

Always validate or trust the source of a string before parsing it, and wrap JSON.parse calls on external input (API responses, localStorage, user-provided files) in a try/catch block.

Mistake 3: assuming every value type survives a stringify round trip. Functions, undefined, and symbols are dropped from objects; NaN and Infinity become null because JSON has no way to represent them.

const data = {
  score: NaN,
  ratio: Infinity,
  callback: () => {},
  id: Symbol("id"),
  label: undefined
};

console.log(JSON.stringify(data));
Output:
{"score":null,"ratio":null}

Only score and ratio survive, both silently downgraded to null. The other three keys vanish entirely. If your data depends on any of these types, convert them to a JSON-safe representation (a string, a number, or a custom encoding) before calling stringify.

Best Practices

  • Always wrap JSON.parse() in a try/catch when the input comes from outside your program (network responses, localStorage, files, user input).
  • Use the space argument (e.g. JSON.stringify(value, null, 2)) for logs, config files, and anything a human will read; omit it for data sent over the network to save bytes.
  • Define a toJSON() method on custom classes when you want control over how instances serialize, instead of relying on default enumeration of their properties.
  • Use a replacer function to strip sensitive fields (passwords, tokens, internal IDs) before sending an object to a client or logging it.
  • Use a reviver function to reconstruct rich types like Date after parsing, rather than manually walking the parsed result afterward.
  • Never rely on JSON.stringify/JSON.parse to deep-clone data that contains functions, circular references, Map, Set, or undefined values — use structuredClone() for a proper deep clone instead.
  • Remember object key order is preserved for string keys in the order they were created (with integer-like keys sorted first), but don’t design logic that depends on JSON key ordering across different systems.

Practice Exercises

1. Write a function toPrettyJson(value) that returns a JSON string of value indented with 4 spaces, and test it on an object with nested arrays.

2. Create an object containing a Map and a regular array. Use a replacer function passed to JSON.stringify to convert the Map into a plain array of [key, value] pairs so it serializes correctly, then write a matching reviver that turns it back into a Map when parsed.

3. Given a JSON string representing a list of products (each with a price field as a number), parse it and write code that sums the total price. Then deliberately corrupt the string (remove a closing brace) and confirm your code handles the resulting SyntaxError gracefully with a try/catch.

Summary

  • JSON is a strict, text-based data format: double-quoted keys and strings only, no comments, no trailing commas, and a limited set of value types.
  • JSON.stringify(value, replacer, space) converts a JavaScript value into a JSON string; JSON.parse(text, reviver) converts it back.
  • Values with a toJSON() method (like Date) are converted using that method before serialization.
  • Functions, undefined, and symbols are dropped from objects (and turned into null inside arrays); NaN and Infinity become null.
  • A replacer function lets you filter or transform values during stringify; a reviver function lets you transform values during parse, working from the innermost values outward.
  • Circular references throw a TypeError during stringify; malformed JSON text throws a SyntaxError during parse — always guard external input with try/catch.
  • JSON.parse always produces brand-new plain objects; it never restores class instances or object identity.