JSON methods, toJSON
You have an object in memory. You want it somewhere else: written to a log, saved to disk, sent to a server across the network. Memory can’t travel, but text can. So the recurring problem is turning a live object into a string, and later rebuilding the object from that string.
You could hand-roll the conversion. Give the object a toString method that spells out its shape:
let user = {
name: "Maya",
age: 30,
toString() {
return `{name: "${this.name}", age: ${this.age}}`;
}
};
alert(user); // {name: "Maya", age: 30}
That works until the object changes. Add a field, rename one, remove another, and you have to hand-edit toString to match. Now imagine the object holds nested objects, and each of those needs its own serialization. The manual approach collapses under its own weight fast.
You don’t have to write any of it. This is a solved problem, and the solution ships with the language.
JSON.stringify
JSON (JavaScript Object Notation) is a text format for representing values and objects. It’s specified in RFC 8259 (which replaced the older RFC 4627). The syntax came from JavaScript, but the format is language-neutral: Python, Ruby, PHP, Java, Go, and nearly everything else has a JSON library. That’s the whole appeal. A JavaScript client can hand a JSON payload to a server written in any language, and both sides understand it.
JavaScript gives you two methods for the round trip:
JSON.stringify— object to JSON string (serialize).JSON.parse— JSON string back to object (deserialize).
Here’s JSON.stringify applied to a student record:
let student = {
name: 'Maya',
age: 30,
isAdmin: false,
courses: ['html', 'css', 'js'],
spouse: null
};
let json = JSON.stringify(student);
alert(typeof json); // we've got a string!
alert(json);
/* JSON-encoded object:
{
"name": "Maya",
"age": 30,
"isAdmin": false,
"courses": ["html", "css", "js"],
"spouse": null
}
*/
JSON.stringify(student) walks the object and returns a single string. That string goes by several names — JSON-encoded, serialized, stringified, or marshalled. All describe the same result: a flat piece of text ready to travel over the wire or land in a data store.
Change the fields below and watch the encoded string update. Notice that every key and every text value gets wrapped in double quotes, while numbers and booleans stay bare:
The output looks close to a JavaScript object literal, but a few differences are strict, not stylistic:
- Strings are wrapped in double quotes. JSON has no single quotes and no backticks, so
'Maya'comes out as"Maya". - Property names are double-quoted too, always.
age: 30becomes"age": 30.
Primitives work too
JSON.stringify isn’t limited to objects. Hand it a primitive and you get that primitive’s JSON form:
// a number in JSON is just a number
alert( JSON.stringify(1) ) // 1
// a string in JSON is still a string, but double-quoted
alert( JSON.stringify('test') ) // "test"
alert( JSON.stringify(true) ); // true
alert( JSON.stringify([1, 2, 3]) ); // [1,2,3]
The supported types
JSON is deliberately small. It supports exactly these:
- Objects
{ ... } - Arrays
[ ... ] - Primitives: strings, numbers, booleans (
true/false), andnull.
Because JSON is a data-only, language-independent format, anything that’s purely a JavaScript notion gets dropped by JSON.stringify. Specifically:
- Function properties (methods).
- Symbolic keys and values (properties keyed by a
Symbol). - Properties holding
undefined.
let user = {
sayHi() { // ignored
alert("Hello");
},
[Symbol("id")]: 123, // ignored
something: undefined // ignored
};
alert( JSON.stringify(user) ); // {} (empty object)
Every property here is one of the skipped kinds, so the result is an empty object. Usually that’s what you want. When it isn’t, you can override the behavior — the replacer argument and a custom toJSON (both below) give you control.
Nesting is automatic
The best part: nested objects and arrays serialize on their own, all the way down. You don’t lift a finger.
let webinar = {
topic: "Product Launch",
hall: {
capacity: 40,
speakers: ["maya", "priya"]
}
};
alert( JSON.stringify(webinar) );
/* The whole structure is stringified:
{
"topic":"Product Launch",
"hall":{"capacity":40,"speakers":["maya","priya"]}
}
*/
The one hard limit: no circular references
There’s a catch that will bite you eventually. The object graph must be a tree, not a loop. If two objects point at each other, JSON.stringify has no way to write out an infinite chain, so it throws.
let hall = {
capacity: 40
};
let webinar = {
topic: "Product Launch",
speakers: ["maya", "priya"]
};
webinar.venue = hall; // webinar references hall
hall.bookedFor = webinar; // hall references webinar
JSON.stringify(webinar); // Error: Converting circular structure to JSON
webinar.venue points to hall, and hall.bookedFor points right back to webinar. Following the links never terminates.
The next section shows how to cut a link like bookedFor so the rest can still serialize.
Excluding and transforming: replacer
The full signature of JSON.stringify takes three arguments:
let json = JSON.stringify(value[, replacer, space])
- value
The value to encode.
- replacer
An array of property names to keep, or a mapping function
function(key, value).
- space
How much indentation to add for pretty-printing.
Most of the time you pass just the value. But when you need to filter the output — say, to drop the circular link from earlier — the second argument earns its keep.
An allow-list array
Pass an array of property names and only those properties survive:
let hall = {
capacity: 40
};
let webinar = {
topic: "Product Launch",
speakers: [{name: "Maya"}, {name: "Nadia"}],
venue: hall // webinar references hall
};
hall.bookedFor = webinar; // hall references webinar
alert( JSON.stringify(webinar, ['topic', 'speakers']) );
// {"topic":"Product Launch","speakers":[{},{}]}
That’s a bit too aggressive. The allow-list applies to the whole structure, at every level. Since name isn’t on the list, the speaker objects come out empty — [{},{}].
So widen the list to name every property you want to keep, everywhere in the graph, while leaving out bookedFor (the one that closes the loop):
let hall = {
capacity: 40
};
let webinar = {
topic: "Product Launch",
speakers: [{name: "Maya"}, {name: "Nadia"}],
venue: hall // webinar references hall
};
hall.bookedFor = webinar; // hall references webinar
alert( JSON.stringify(webinar, ['topic', 'speakers', 'venue', 'name', 'capacity']) );
/*
{
"topic":"Product Launch",
"speakers":[{"name":"Maya"},{"name":"Nadia"}],
"venue":{"capacity":40}
}
*/
Everything except bookedFor makes it through. It works, but the list is long and you have to remember to keep it in sync with the object. For anything but a small fixed shape, that’s fragile.
A replacer function
Instead of listing properties, hand JSON.stringify a function. It runs once for every (key, value) pair and returns the value to use in the output. Return the value unchanged to keep it, or return undefined to drop that property entirely.
For the circular case, the rule is simple: keep everything, except skip bookedFor.
let hall = {
capacity: 40
};
let webinar = {
topic: "Product Launch",
speakers: [{name: "Maya"}, {name: "Nadia"}],
venue: hall // webinar references hall
};
hall.bookedFor = webinar; // hall references webinar
alert( JSON.stringify(webinar, function replacer(key, value) {
alert(`${key}: ${value}`);
return (key == 'bookedFor') ? undefined : value;
}));
/* key:value pairs that come to replacer:
: [object Object]
topic: Product Launch
speakers: [object Object],[object Object]
0: [object Object]
name: Maya
1: [object Object]
name: Nadia
venue: [object Object]
capacity: 40
bookedFor: [object Object]
*/
The alert inside the function traces every pair the replacer sees, and there’s a lot to notice there.
The replacer visits every key/value pair, including entries nested deep inside objects and arrays — array indices (0, 1) show up as keys. It runs top-down and recursively. Inside the function, this is the object that currently holds the property being visited.
That first line is special. The very first call uses a synthetic wrapper object, {"": webinar}. So the first pair has an empty-string key and the entire target object as its value — which is why the trace opens with ":[object Object]".
Why bother with the wrapper? It hands the replacer maximum reach. Even the top-level object passes through the function, so you can inspect, transform, or replace the whole thing before anything else runs.
Formatting: space
The third argument, space, controls pretty-printing — how many spaces to indent nested levels.
Everything so far came out as one dense line with no spaces. That’s ideal for transport: fewer bytes, nothing wasted. But it’s painful to read. space exists purely to make the output human-friendly.
Pass 2 and each nesting level gets a two-space indent, with nested objects broken onto their own lines:
let user = {
name: "Maya",
age: 25,
roles: {
isAdmin: false,
isEditor: true
}
};
alert(JSON.stringify(user, null, 2));
/* two-space indents:
{
"name": "Maya",
"age": 25,
"roles": {
"isAdmin": false,
"isEditor": true
}
}
*/
/* for JSON.stringify(user, null, 4) the result would be more indented:
{
"name": "Maya",
"age": 25,
"roles": {
"isAdmin": false,
"isEditor": true
}
}
*/
Note the null in the middle slot — that’s the replacer argument, left empty here because we only want formatting.
space can also be a string, in which case that string is used for each indent step instead of a count of spaces. Passing '\t' indents with tabs, for example.
Drag the slider to feel the difference. At 0 you get the compact transport form; bump it up and the same data spreads out for reading:
Custom “toJSON”
Just as an object can define toString to steer string conversion, it can define toJSON to steer JSON conversion. When JSON.stringify hits a value, it checks for a toJSON method and, if there is one, uses whatever that method returns in place of the object.
You’ve already benefited from this without noticing. Date objects have a built-in toJSON:
let hall = {
capacity: 40
};
let webinar = {
topic: "Product Launch",
date: new Date(Date.UTC(2017, 0, 1)),
hall
};
alert( JSON.stringify(webinar) );
/*
{
"topic":"Product Launch",
"date":"2017-01-01T00:00:00.000Z", // (1)
"hall": {"capacity":40} // (2)
}
*/
At (1), date came out as an ISO string. That’s Date.prototype.toJSON doing its job — it returns that standardized string, and JSON.stringify uses it.
Now give hall (2) its own toJSON:
let hall = {
capacity: 40,
toJSON() {
return this.capacity;
}
};
let webinar = {
topic: "Product Launch",
hall
};
alert( JSON.stringify(hall) ); // 40
alert( JSON.stringify(webinar) );
/*
{
"topic":"Product Launch",
"hall": 40
}
*/
toJSON kicks in both ways: when you stringify hall directly, and when hall sits nested inside webinar. Either way, the object is replaced by its toJSON result before encoding.
JSON.parse
Serialization is only half the trip. To turn a JSON string back into a live object, use JSON.parse.
let value = JSON.parse(str[, reviver]);
- str
The JSON string to parse.
- reviver
Optional
function(key, value)called for every pair, letting you transform values as they’re read.
A stringified array comes back as a real array:
// stringified array
let numbers = "[0, 1, 2, 3]";
numbers = JSON.parse(numbers);
alert( numbers[1] ); // 1
Nested structures rebuild fully:
let userData = '{ "name": "Maya", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';
let user = JSON.parse(userData);
alert( user.friends[1] ); // 1
The input can be arbitrarily deep — objects inside arrays inside objects — as long as it’s valid JSON.
Common hand-written JSON mistakes
Sometimes you write JSON by hand, usually while debugging. That’s where the strict rules trip people up. Every line below except the last is invalid:
let json = `{
name: "Maya", // mistake: property name without quotes
"surname": 'Vance', // mistake: single quotes in value (must be double)
'isAdmin': false // mistake: single quotes in key (must be double)
"birthday": new Date(2000, 2, 3), // mistake: no "new" is allowed, only bare values
"friends": [0,1,2,3] // here all fine
}`;
There’s a separate format, JSON5, that relaxes these rules — unquoted keys, comments, trailing commas, and more. But JSON5 is a third-party library, not part of the JavaScript language or the JSON standard.
The strictness isn’t laziness on the spec authors’ part. Tight, unambiguous rules are exactly what let parsers be simple, fast, and reliable across every language that implements them.
JSON.parse throws on bad input
One more thing worth internalizing: invalid JSON doesn’t return null or undefined, it throws a SyntaxError. Any code parsing data you didn’t produce yourself — a network response, a file, user input — should wrap the call in try...catch:
try {
let data = JSON.parse(maybeBrokenString);
} catch (e) {
alert("Bad JSON: " + e.message);
}
Try it yourself. Edit the text below — remove a quote, add a trailing comma, use single quotes — and hit Parse. Valid input reports the parsed keys; invalid input shows the exact SyntaxError caught by try...catch:
Using reviver
Say the server sends you a stringified webinar:
// topic: (webinar topic), date: (webinar date)
let str = '{"topic":"Product Launch","date":"2017-11-30T12:00:00.000Z"}';
You need to deserialize it into a usable object. Straightforward parse:
let str = '{"topic":"Product Launch","date":"2017-11-30T12:00:00.000Z"}';
let webinar = JSON.parse(str);
alert( webinar.date.getDate() ); // Error!
It throws. And the reason is the same limitation as before: webinar.date is a plain string, not a Date. JSON has no date type, so the date arrived as text. JSON.parse had no way to know that particular string was meant to become a Date object.
This is where the second argument, the reviver, comes in. It’s a function that runs on every parsed pair, and its return value replaces the parsed one. Return everything as-is, except turn date into a real Date:
let str = '{"topic":"Product Launch","date":"2017-11-30T12:00:00.000Z"}';
let webinar = JSON.parse(str, function(key, value) {
if (key == 'date') return new Date(value);
return value;
});
alert( webinar.date.getDate() ); // now works!
The same reviver handles nested data without extra work, because it visits every pair at every depth:
let schedule = `{
"webinars": [
{"topic":"Product Launch","date":"2017-11-30T12:00:00.000Z"},
{"topic":"Design Review","date":"2017-04-18T12:00:00.000Z"}
]
}`;
schedule = JSON.parse(schedule, function(key, value) {
if (key == 'date') return new Date(value);
return value;
});
alert( schedule.webinars[1].date.getDate() ); // works!
Toggle the reviver on and off to see the difference concretely. Without it, date stays a string and calling a Date method fails; with it, date comes back as a real Date you can query:
Summary
- JSON is a compact, text-based data format with its own standard, supported by libraries in most programming languages.
- It represents plain objects, arrays, strings, numbers, booleans, and
null— and nothing else. Functions, symbols, andundefinedare dropped;NaN/Infinitybecomenull;BigIntthrows. JSON.stringifyserializes a value to JSON;JSON.parsebuilds a value back from JSON. Bad input makesJSON.parsethrow, so guard it.- Object graphs must be trees. Circular references make
JSON.stringifythrow — use areplacerto cut the offending link. JSON.stringifytakes areplacer(an allow-list array or a mapping function) and aspaceargument for pretty-printing.JSON.parsetakes areviverto transform values as they’re read.- If an object defines
toJSON,JSON.stringifycalls it and encodes the result instead — that’s howDateserializes to an ISO string automatically.