JavaScript JSON: 7 Methods Most Developers Get Wrong in 2026
[rank_math_toc]
JavaScript JSON is the backbone of every web application that talks to a server—and yet most developers treat JSON.parse() and JSON.stringify() as black boxes. They call them, hope for the best, and move on. That’s a mistake. These two methods have hidden parameters that unlock powerful serialization patterns, and ignoring them leads to bugs that are incredibly hard to track down. In this lesson, you’ll learn everything about JSON in JavaScript—from the basics to the advanced tricks that separate mid-level developers from senior engineers.
What Is JSON and Why JavaScript Owns It
JSON (JavaScript Object Notation) was born from JavaScript’s object literal syntax. Douglas Crockford formalized it in the early 2000s, and it quickly replaced XML as the dominant data interchange format. The beauty of JSON is its simplicity: it supports strings, numbers, booleans, null, arrays, and objects. That’s it. No comments, no trailing commas, no undefined. This constraint is what makes it universally parseable across every programming language on the planet.
But here’s what trips people up: JSON is not JavaScript. A JavaScript object can hold functions, undefined, Symbols, and circular references. JSON cannot. Understanding this gap is critical, and we’ll dig into every edge case below. If you need a refresher on JavaScript objects, check out our Objects lesson.
JSON.parse() — From String to JavaScript Object
JSON.parse() takes a JSON string and converts it into a JavaScript value. Simple enough on the surface:
const jsonString = '{"name": "Alice", "age": 30, "active": true}';
const user = JSON.parse(jsonString);
console.log(user.name); // "Alice"
console.log(user.age); // 30
console.log(typeof user); // "object"
But JSON.parse() is strict. Any malformed input throws a SyntaxError. You should always wrap it in a try/catch block:
function safeParse(str) {
try {
return JSON.parse(str);
} catch (err) {
console.error("Invalid JSON:", err.message);
return null;
}
}
safeParse('{"valid": true}'); // { valid: true }
safeParse('{invalid json}'); // null, logs error
safeParse('undefined'); // null, logs error
The Reviver Parameter — JSON.parse()’s Secret Weapon
Most developers don’t know that JSON.parse() accepts a second argument: a reviver function. This function is called for every key-value pair during parsing, giving you the power to transform values on the fly.
The classic use case is restoring Date objects. JSON doesn’t have a Date type, so dates get serialized as strings. The reviver brings them back:
const data = '{"created": "2026-05-07T10:30:00.000Z", "name": "Report"}';
const parsed = JSON.parse(data, (key, value) => {
// Check if the value looks like an ISO date string
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
return new Date(value);
}
return value;
});
console.log(parsed.created instanceof Date); // true
console.log(parsed.created.getFullYear()); // 2026
The reviver walks the parsed result bottom-up. The final call has key as an empty string "" and value as the entire parsed object. This lets you do whole-object transformations:
const result = JSON.parse('{"x": 1, "y": 2}', (key, value) => {
if (key === "") return value; // root object, return as-is
return value * 10; // multiply all numeric values
});
console.log(result); // { x: 10, y: 20 }
JSON.stringify() — From JavaScript to JSON String
JSON.stringify() converts a JavaScript value into a JSON string. But it takes three parameters, and the second and third are where the real power lives.
// Basic usage
JSON.stringify({ name: "Bob", age: 25 });
// '{"name":"Bob","age":25}'
// With indentation (third parameter)
JSON.stringify({ name: "Bob", age: 25 }, null, 2);
// '{
// "name": "Bob",
// "age": 25
// }'
The Replacer Parameter — Filtering and Transforming Output
The second parameter is the replacer. It can be a function or an array. As a function, it works like the reviver but in reverse—you control what gets serialized:
const user = {
name: "Alice",
password: "secret123",
email: "alice@example.com",
role: "admin"
};
// Remove sensitive fields
const safe = JSON.stringify(user, (key, value) => {
if (key === "password") return undefined; // exclude this key
return value;
}, 2);
console.log(safe);
// {
// "name": "Alice",
// "email": "alice@example.com",
// "role": "admin"
// }
As an array, the replacer acts as a whitelist—only the specified keys are included:
const user = { name: "Alice", password: "secret", email: "alice@ex.com" };
JSON.stringify(user, ["name", "email"]);
// '{"name":"Alice","email":"alice@ex.com"}'
This is cleaner than a function replacer when you just want to pick specific fields. It’s essentially destructuring for serialization.
Deep Cloning: structuredClone vs JSON Roundtrip
For years, the JSON.parse(JSON.stringify(obj)) trick was the go-to deep clone method. It works, but it has serious limitations:
// The classic deep clone trick
const original = { a: 1, b: { c: 2 } };
const clone = JSON.parse(JSON.stringify(original));
clone.b.c = 99;
console.log(original.b.c); // 2 — properly deep cloned
// But it fails on Dates, Maps, Sets, undefined, functions...
const problematic = {
date: new Date(),
set: new Set([1, 2, 3]),
fn: () => "hello",
undef: undefined
};
const broken = JSON.parse(JSON.stringify(problematic));
console.log(broken.date); // string, not Date object!
console.log(broken.set); // {} — empty object!
console.log(broken.fn); // undefined — gone!
console.log(broken.undef); // key doesn't exist!
Since 2022, structuredClone() is the proper solution. It handles Dates, Maps, Sets, ArrayBuffers, RegExps, and even circular references:
const original = {
date: new Date(),
set: new Set([1, 2, 3]),
map: new Map([["key", "value"]]),
nested: { deep: { value: 42 } }
};
const clone = structuredClone(original);
console.log(clone.date instanceof Date); // true
console.log(clone.set instanceof Set); // true
console.log(clone.map.get("key")); // "value"
Use structuredClone() for deep cloning. Use the JSON roundtrip only when you specifically need JSON-safe data (like preparing data for an API call). For more on how JavaScript handles different data types, see our Data Types lesson.
JSON Gotchas That Break Production Code
These are the edge cases that cause real bugs. Memorize them:
1. undefined and Functions Vanish
const obj = { a: undefined, b: function() {}, c: "kept" };
JSON.stringify(obj);
// '{"c":"kept"}' — a and b are gone
// In arrays, they become null instead of disappearing
JSON.stringify([undefined, function() {}, "kept"]);
// '[null,null,"kept"]'
2. Dates Become Strings
const date = new Date("2026-01-15");
const json = JSON.stringify({ created: date });
// '{"created":"2026-01-15T00:00:00.000Z"}'
const parsed = JSON.parse(json);
console.log(typeof parsed.created); // "string" — NOT a Date!
3. BigInt Throws an Error
const big = { value: 9007199254740993n };
JSON.stringify(big);
// TypeError: Do not know how to serialize a BigInt
4. Circular References Crash
const obj = { name: "circular" };
obj.self = obj; // circular reference
JSON.stringify(obj);
// TypeError: Converting circular structure to JSON
5. NaN and Infinity Become null
JSON.stringify({ a: NaN, b: Infinity, c: -Infinity });
// '{"a":null,"b":null,"c":null}'
Understanding how JavaScript handles these special number values is critical when working with JSON in production.
The toJSON() Method — Custom Serialization
Any object can define a toJSON() method that controls how it gets serialized. JSON.stringify() calls this method automatically if it exists:
class User {
constructor(name, email, password) {
this.name = name;
this.email = email;
this.password = password;
this.createdAt = new Date();
}
toJSON() {
return {
name: this.name,
email: this.email,
createdAt: this.createdAt.toISOString(),
// password deliberately excluded
};
}
}
const user = new User("Alice", "alice@example.com", "secret123");
console.log(JSON.stringify(user, null, 2));
// {
// "name": "Alice",
// "email": "alice@example.com",
// "createdAt": "2026-05-07T..."
// }
This pattern is extremely useful in class-based architectures. It’s how Date.prototype.toJSON() works internally—it calls toISOString().
Working With API Data: fetch + JSON
In the real world, JSON’s primary role is communicating with APIs. Here’s the complete pattern using fetch, including proper error handling:
// GET request — reading JSON from an API
async function getUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const user = await response.json(); // built-in JSON parsing
return user;
}
// POST request — sending JSON to an API
async function createUser(userData) {
const response = await fetch("https://api.example.com/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userData) // must stringify manually
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response.json();
}
// Usage
try {
const newUser = await createUser({
name: "Alice",
email: "alice@example.com"
});
console.log("Created:", newUser.id);
} catch (err) {
console.error("Failed:", err.message);
}
Note that response.json() is just a convenience method—it calls JSON.parse() on the response body internally. If the response isn’t valid JSON, it throws.
JSON5, JSONL, and Real-World Formats
Standard JSON is strict by design, but several variants exist for specific use cases:
JSON5 extends JSON with JavaScript-like syntax: comments, trailing commas, unquoted keys, single-quoted strings, and hex numbers. It’s commonly used in config files (.babelrc, tsconfig.json actually uses a JSON5-like parser). You need the json5 library to parse it.
JSONL (JSON Lines) puts one JSON object per line, with no wrapping array. This format is used heavily in logging, data pipelines, and streaming APIs (including OpenAI’s streaming responses):
// Parsing JSONL
const jsonl = `{"id":1,"name":"Alice"}
{"id":2,"name":"Bob"}
{"id":3,"name":"Charlie"}`;
const records = jsonl
.split("\n")
.filter(line => line.trim())
.map(line => JSON.parse(line));
console.log(records.length); // 3
NDJSON is essentially the same as JSONL—newline-delimited JSON. The terms are used interchangeably in practice.
Performance Tips and Interview Questions
For large datasets, JSON.parse() is surprisingly fast—often faster than manually constructing objects. V8 (Chrome/Node.js) has heavily optimized its JSON parser. However, JSON.stringify() on deeply nested objects can be slow. If you’re serializing the same structure repeatedly, consider using a schema-based serializer like fast-json-stringify which can be 2-5x faster.
Common interview question: “How do you deep clone an object in JavaScript?” The expected answer progression is: (1) structuredClone() for modern code, (2) JSON roundtrip for JSON-safe data, (3) recursive manual clone for full control. Mentioning the limitations of each approach shows depth. Review our Objects lesson for more on reference types.
Another favorite: “What happens when you JSON.stringify() a value that contains undefined?” Answer: it depends on context—properties with undefined values are omitted from objects, but become null in arrays.
Practical Exercise
Build a local storage wrapper that handles serialization automatically:
const storage = {
set(key, value) {
const serialized = JSON.stringify(value, (k, v) => {
if (v instanceof Date) return { __type: "Date", value: v.toISOString() };
if (v instanceof Set) return { __type: "Set", value: [...v] };
if (v instanceof Map) return { __type: "Map", value: [...v] };
return v;
});
localStorage.setItem(key, serialized);
},
get(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
return JSON.parse(raw, (k, v) => {
if (v && v.__type === "Date") return new Date(v.value);
if (v && v.__type === "Set") return new Set(v.value);
if (v && v.__type === "Map") return new Map(v.value);
return v;
});
}
};
// Now it handles Dates, Sets, and Maps transparently
storage.set("prefs", {
theme: "dark",
lastVisit: new Date(),
favorites: new Set(["js", "python", "rust"])
});
const prefs = storage.get("prefs");
console.log(prefs.lastVisit instanceof Date); // true
console.log(prefs.favorites instanceof Set); // true
This pattern is production-ready and demonstrates the reviver/replacer combo at its best. For more on how strings work under the hood in JavaScript, check our dedicated lesson.
Key Takeaways
JSON.parse()accepts a reviver function to transform values during parsingJSON.stringify()accepts a replacer (function or array) and an indentation parameter- Use
structuredClone()for deep cloning—not the JSON roundtrip hack undefined, functions, and Symbols are silently dropped during serialization- BigInt throws; Dates become strings; NaN/Infinity become null
- Define
toJSON()on classes for custom serialization control - Always wrap
JSON.parse()in try/catch in production code
Next up, we’ll start working with the browser’s DOM. Head to the Selecting Elements lesson to learn how to find and target HTML elements with JavaScript.
External Resources:
- MDN — JSON Reference
- javascript.info — JSON Methods
- MDN — structuredClone()
- JSON.org — Official Specification
- V8 Blog — The Cost of JavaScript