JSON Format Rules
JSON is a strict subset of JavaScript object notation with several important differences.
| Feature | JSON | JavaScript Object Literal |
|---|---|---|
| Keys | Must be double-quoted strings | Unquoted identifiers allowed |
| Strings | Double quotes only | Single, double, or backtick |
| Functions | Not allowed | Allowed |
undefined | Not allowed (silently dropped) | Allowed |
| Comments | Not allowed | Allowed |
| Trailing commas | Not allowed | Allowed in modern JS |
NaN / Infinity | Not allowed | Allowed |
JSON.parse()
JSON.parse() converts a JSON string into a JavaScript value. It throws a SyntaxError if the string is not valid JSON.
// Basic parsing
const jsonString = '{"name":"Alice","age":28,"hobbies":["coding","reading"]}';
const user = JSON.parse(jsonString);
console.log(user.name); // 'Alice'
console.log(user.age); // 28 (number, not string)
console.log(user.hobbies[1]); // 'reading'
// Parse all JSON types
JSON.parse('"hello"'); // 'hello' (string)
JSON.parse('42'); // 42 (number)
JSON.parse('true'); // true (boolean)
JSON.parse('null'); // null
JSON.parse('[1,2,3]'); // [1, 2, 3] (array)
// Error handling (always wrap in try/catch!)
function safeParse(text, fallback = null) {
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
console.log(safeParse('{"a":1}')); // {a: 1}
console.log(safeParse('not json')); // null
console.log(safeParse('bad', {})); // {}
JSON.stringify()
JSON.stringify() converts a JavaScript value to a JSON string. It accepts an optional replacer and a space argument for pretty-printing.
const user = {
name: 'Alice',
age: 28,
password: 'secret123', // we want to exclude this
greet() { return 'Hello'; }, // functions are silently dropped
createdAt: new Date()
};
// Basic serialisation
const json = JSON.stringify(user);
// '{"name":"Alice","age":28,"createdAt":"2026-06-03T..."}'
// Note: password β wait, it IS included! See replacer below.
// Note: greet() is dropped (functions not in JSON)
// Pretty-print with 2-space indent
const pretty = JSON.stringify(user, null, 2);
console.log(pretty);
/*
{
"name": "Alice",
"age": 28,
...
}
*/
// Replacer as array β whitelist of keys to include
const safe = JSON.stringify(user, ['name', 'age'], 2);
// Only name and age are included β password excluded!
// Replacer as function β called for each key/value
const filtered = JSON.stringify(user, (key, value) => {
if (key === 'password') return undefined; // exclude
if (typeof value === 'number') return value * 2; // transform
return value;
}, 2);
{
"name": "Alice",
"age": 28,
"createdAt": "2026-06-03T12:00:00.000Z"
}
// safe: { "name": "Alice", "age": 28 }
// filtered: { "name": "Alice", "age": 56, "createdAt": "..." }
JSON.parse with Reviver
The reviver function is called for each parsed key/value pair, letting you transform values as JSON is parsed β for example, converting date strings back to Date objects.
const data = '{"name":"Alice","birthDate":"1998-03-15T00:00:00.000Z","score":42}';
// Reviver: convert date strings to Date objects
const parsed = JSON.parse(data, (key, value) => {
// ISO date string pattern
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
return new Date(value);
}
return value;
});
console.log(parsed.name); // 'Alice'
console.log(parsed.birthDate); // Date object (not string!)
console.log(parsed.birthDate instanceof Date); // true
// Custom serialisation with toJSON
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
toJSON() {
// Custom serialisation format
return `${this.amount} ${this.currency}`;
}
}
const price = new Money(29.99, 'USD');
console.log(JSON.stringify({ price })); // '{"price":"29.99 USD"}'
Handling Dates in JSON
// Dates are serialised to ISO 8601 strings by stringify
const event = { title: 'Meeting', date: new Date('2026-06-15') };
const json = JSON.stringify(event);
// '{"title":"Meeting","date":"2026-06-15T00:00:00.000Z"}'
// After parsing, date is a STRING again, not a Date
const parsed = JSON.parse(json);
console.log(typeof parsed.date); // 'string'
console.log(parsed.date instanceof Date); // false
// Solution 1: reviver (as shown above)
const withDates = JSON.parse(json, (k, v) =>
typeof v === 'string' && /Z$/.test(v) ? new Date(v) : v
);
console.log(withDates.date instanceof Date); // true
// Solution 2: store timestamps (numbers)
const eventWithTS = { title: 'Meeting', timestamp: Date.now() };
// Numbers survive JSON round-trips perfectly
// Solution 3: use structuredClone for in-memory cloning (preserves Date type)
const clone = structuredClone(event);
console.log(clone.date instanceof Date); // true
When sending dates over an API, ISO 8601 strings are universally understood. When storing dates in memory, use structuredClone() instead of a JSON round-trip β it preserves Date objects natively without conversion.
Deep Cloning with JSON
const original = {
name: 'Alice',
scores: [95, 82, 88],
address: { city: 'London' }
};
// JSON round-trip deep clone
const clone = JSON.parse(JSON.stringify(original));
clone.address.city = 'Paris';
clone.scores.push(100);
console.log(original.address.city); // 'London' β not affected
console.log(original.scores.length); // 3 β not affected
// JSON clone LIMITATIONS:
const problematic = {
fn: () => 'hello', // silently dropped
undef: undefined, // silently dropped
date: new Date(), // converted to ISO string
regex: /hello/g, // converted to {}
map: new Map([[1,2]]), // converted to {}
nan: NaN, // converted to null
inf: Infinity // converted to null
};
const lostData = JSON.parse(JSON.stringify(problematic));
console.log(lostData);
// { date: '...string...', regex: {}, map: {}, nan: null, inf: null }
// fn and undef are gone
// Prefer structuredClone for reliable deep cloning
const better = structuredClone(original); // handles Date, Set, Map, etc.
Working with API Responses
// Standard fetch + JSON parsing
async function fetchUsers() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const users = await response.json(); // response.json() calls JSON.parse internally
return users;
}
// Sending JSON in a POST request
async function createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // important!
},
body: JSON.stringify(userData)
});
return response.json();
}
// Safe response parsing with validation
async function safeApiCall(url) {
try {
const res = await fetch(url);
const text = await res.text(); // get raw text first
let data;
try {
data = JSON.parse(text);
} catch {
throw new Error(`Server returned non-JSON: ${text.slice(0, 100)}`);
}
if (!res.ok) {
throw new Error(data.message ?? `HTTP ${res.status}`);
}
return data;
} catch (error) {
console.error('API call failed:', error.message);
throw error;
}
}
fetch() does not reject on HTTP error status codes (4xx, 5xx) β it only rejects on network failures. Always check response.ok (or response.status) before parsing. A server can return a 404 or 500 response with a JSON body, and await response.json() will happily parse it without throwing.
Interview Questions
- What values does
JSON.stringifysilently drop? - How would you exclude sensitive fields when serialising an object to JSON?
- Why do
Dateobjects not survive a JSON round-trip? - What is the reviver function in
JSON.parse? - What is the difference between
JSON.parse(JSON.stringify(x))andstructuredClone(x)?
ποΈ Practical Exercise
Write a serialize(obj, sensitiveKeys = []) function that converts an object to a formatted JSON string (2-space indent), automatically excludes any keys listed in sensitiveKeys, and converts Date objects to human-readable strings using toLocaleString() instead of ISO format. Test it with a user object containing password and createdAt fields.
π₯ Challenge Exercise
Implement a JSONStorage class that wraps localStorage with automatic JSON serialisation and a schema validator. It should support set(key, value, schema?), get(key, reviver?), and getOrDefault(key, defaultValue). The optional schema validator should be a function that returns true/false β if validation fails on set, throw a ValidationError.
Frequently Asked Questions
- Why does
JSON.stringify(undefined)returnundefined(not a string)? undefinedis not a valid JSON value. When the top-level value isundefined,JSON.stringifyreturnsundefined(not the string"undefined"). Inside an object, properties withundefinedvalues are silently omitted. Inside an array,undefinedis converted tonull.- Can JSON represent circular references?
- No. If you pass an object with circular references to
JSON.stringify, it throws aTypeError: Converting circular structure to JSON. Use a library likeflattedorcircular-json, or manually break circular references before serialising. - What is the difference between
JSON5and standard JSON? - JSON5 is a superset of JSON that allows single-quoted strings, comments, trailing commas, unquoted object keys, and
Infinity/NaN. It is not natively supported in browsers β you need thejson5npm package. Standard JSON is stricter and universally supported. - Is
response.json()the same asJSON.parse(await response.text())? - Functionally yes β both parse the response body as JSON.
response.json()is a convenience method. One subtle difference:response.json()may handle charset detection from response headers, whileresponse.text()gives you the raw decoded string. - How do I validate the structure of a parsed JSON object?
- Use a schema validation library like Zod, Yup, or Ajv. These let you define the expected shape and types of your data and throw descriptive errors when the structure doesn't match. This is especially important for API responses where you don't control the server.
π Summary
- JSON keys and strings must use double quotes; no comments, trailing commas, or functions.
JSON.parse(text)converts a JSON string to a JavaScript value; wrap in try/catch.JSON.stringify(value, replacer, space)converts a value to a JSON string.- Use the replacer array to whitelist keys; use a replacer function to filter/transform.
- Use the reviver to transform values during parsing (e.g., convert date strings to Dates).
JSON.stringifysilently dropsundefined, functions, and Symbols.- JSON round-trip cloning loses Date types β prefer
structuredClone()for in-memory cloning. - Always check
response.okafterfetch()β it does not throw on 4xx/5xx responses.