Skip to content
JavaScript js objects 4 min read

Working with JSON

JSON (JavaScript Object Notation) is the universal text format for exchanging structured data between systems — APIs, config files, local storage, and message queues all speak it. Although its syntax looks like a JavaScript object literal, JSON is a strict, language-independent format with its own rules. JavaScript ships two built-in methods, JSON.stringify and JSON.parse, to convert between in-memory values and JSON text. Understanding their options (and their sharp edges) is essential for any data-driven application.

JSON syntax vs JavaScript objects

JSON looks deceptively similar to an object literal, but it is far stricter. Every key must be a double-quoted string, strings must use double quotes (never single), and a handful of JavaScript values simply do not exist in JSON.

{
  "name": "Ada",
  "age": 36,
  "languages": ["JS", "Python"],
  "active": true,
  "manager": null
}

The differences that trip people up most often:

FeatureJavaScript objectJSON
Key quotesOptional, single or doubleRequired, double quotes only
String quotesSingle, double, or backtickDouble quotes only
Trailing commasAllowedNot allowed
CommentsAllowedNot allowed
undefined, functionsAllowedNot representable
NaN, InfinityAllowedSerialized as null
Date, Map, SetAllowedNo native form (lost)

JSON is a subset of JavaScript’s value space, not a mirror of it. Anything that isn’t a string, number, boolean, null, array, or plain object needs special handling.

Serializing with JSON.stringify

JSON.stringify(value, replacer?, space?) converts a JavaScript value into a JSON string. Properties with undefined, function, or Symbol values are dropped from objects and become null inside arrays.

const user = {
  id: 1,
  name: "Ada",
  greet: () => "hi",   // dropped (function)
  nickname: undefined, // dropped
};

console.log(JSON.stringify(user));

Output:

{"id":1,"name":"Ada"}

The replacer argument

The second argument shapes what gets serialized. Pass an array of keys to allowlist properties, or a function to transform each value as it is visited.

const account = { id: 7, name: "Ada", password: "secret", token: "xyz" };

// Array form: only include these keys
console.log(JSON.stringify(account, ["id", "name"]));

// Function form: omit anything that looks sensitive
const safe = JSON.stringify(account, (key, value) =>
  ["password", "token"].includes(key) ? undefined : value
);
console.log(safe);

Output:

{"id":7,"name":"Ada"}
{"id":7,"name":"Ada"}

The replacer function is called first with an empty key and the whole object, then recursively for every property. Returning undefined removes that property.

Pretty-printing with space

The third argument controls indentation. Pass a number (spaces per level, max 10) or a string (used literally).

const data = { id: 1, tags: ["a", "b"] };
console.log(JSON.stringify(data, null, 2));

Output:

{
  "id": 1,
  "tags": [
    "a",
    "b"
  ]
}

Customizing output with toJSON

If a value has a toJSON() method, JSON.stringify calls it and serializes the return value instead. This is how Date produces an ISO string, and you can use it on your own classes.

class Money {
  constructor(cents) { this.cents = cents; }
  toJSON() { return (this.cents / 100).toFixed(2); }
}

console.log(JSON.stringify({ price: new Money(1599), at: new Date("2026-06-01") }));

Output:

{"price":"15.99","at":"2026-06-01T00:00:00.000Z"}

Parsing with JSON.parse

JSON.parse(text, reviver?) turns a JSON string back into a JavaScript value. Invalid JSON throws a SyntaxError, so untrusted input should be wrapped in try/catch.

const json = '{"id":1,"created":"2026-06-01T00:00:00.000Z"}';
const obj = JSON.parse(json);
console.log(typeof obj.created); // still a string!

Output:

string

The reviver argument

The optional reviver function lets you transform values during parsing — the natural place to rehydrate dates or other rich types. It runs bottom-up, visiting nested values before their parents.

const json = '{"id":1,"created":"2026-06-01T00:00:00.000Z"}';

const obj = JSON.parse(json, (key, value) =>
  key === "created" ? new Date(value) : value
);

console.log(obj.created instanceof Date);

Output:

true

Deep-clone caveats

A popular trick is JSON.parse(JSON.stringify(obj)) to deep-clone an object. It works for plain JSON-safe data but silently corrupts anything else: functions and undefined vanish, Date becomes a string, Map/Set become {}, and circular references throw.

const original = { when: new Date(), items: new Set([1, 2]), fn: () => 1 };
console.log(JSON.parse(JSON.stringify(original)));

Output:

{ when: '2026-06-01T...Z', items: {} }

For real deep cloning, prefer the built-in structuredClone(value) (Node 17+, all modern browsers). It preserves Date, Map, Set, typed arrays, and handles cycles — though it still cannot clone functions.

Common errors

  • Unexpected token — usually trailing commas, single quotes, or unquoted keys in the source text.
  • Circular structureJSON.stringify throws TypeError: Converting circular structure to JSON when an object references itself.
  • Lost precisionBigInt cannot be serialized and throws; large integers beyond Number.MAX_SAFE_INTEGER lose precision on parse.
  • Empty stringJSON.parse("") throws; guard against empty bodies before parsing API responses.

Best Practices

  • Always wrap JSON.parse of external input in try/catch and validate the shape before trusting it.
  • Use a replacer (or a dedicated DTO) to strip secrets like passwords and tokens before sending data over the wire.
  • Reach for structuredClone instead of the stringify/parse clone hack for non-trivial objects.
  • Use a reviver to convert ISO date strings back into Date objects at the parse boundary, not scattered through your code.
  • Pretty-print with JSON.stringify(value, null, 2) for human-facing files and logs; keep wire payloads compact.
  • Define toJSON() on domain classes so serialization stays consistent and centralized.
  • Never assume JSON.stringify round-trips perfectly — test with undefined, Date, and nested structures.
Last updated June 1, 2026
Was this helpful?