JSON
Send and store data as text with JSON.stringify, JSON.parse, replacers, revivers, toJSON, and the newest source-text tools.
- 01Turn data into text and backUse
JSON.stringifyto turn plain objects and arrays into JSON text, andJSON.parseto read the text back. - 02Change values on the wayRemove values with a replacer, rebuild values with a reviver, and give an object its own
toJSONmethod. - 03Know what JSON losesPredict which values JSON leaves out, changes, or refuses, and keep big numbers exact with the newest tools.
JSON: data written as text
JSON is a way to write data as plain text. The name is short for JavaScript Object Notation. "Notation" just means a way of writing things down.
JSON text can hold only these kinds of values:
- Strings in double quotes, like
"Asha". - Numbers, like
3or2.5.NaNandInfinityare not allowed. trueandfalse.null.- Arrays, like
[1, 2]. - Plain objects, like
{"name": "Asha"}. Every property name is in double quotes.
Because JSON is text, you can send it to a server or save it in a file. That is what JSON is for: sending and storing data.
JSON looks like JavaScript objects and arrays, but its rules are stricter. JSON text has:
- No comments.
- No strings in single quotes.
- No functions.
- No
undefined. - No symbols.
- No BigInt values, like
1n. - No special value for dates.
These rules keep JSON small and simple. So almost every programming language can read JSON, not just JavaScript.
Imagine you send a parcel to another country. Many people handle it on the way, and they speak different languages. So the label uses a simple form that everyone knows: a name, an address, a weight, and a few yes/no boxes. Anyone can read it. JSON does the same job for programs. It uses only simple values, so programs written in other languages can read it too.
- In real life: Writing the label before the parcel leaves
- In JavaScript:
JSON.stringify(value)writes JSON text - In real life: Reading the label when the parcel arrives
- In JavaScript:
JSON.parse(text)reads JSON text and makes JavaScript data - In real life: Fields on the label: name, address, weight
- In JavaScript: An object with named properties
- In real life: A list of items in the parcel
- In JavaScript: An array
- In real life: Words, counts, yes/no boxes, and blank fields
- In JavaScript: Strings, numbers, booleans, and
null
You already know objects, numbers, BigInt, and symbols from earlier lessons. This lesson shows what happens to each of them when you turn them into JSON text. Later, the Fetch & JSON lesson in Stage 7 uses the same ideas to read data from servers.
JSON.stringify and JSON.parse
INTERACTIVEJavaScript has a built-in JSON object with two main methods:
JSON.stringify(value)turns a JavaScript value into JSON text. "Stringify" means "make into a string".JSON.parse(text)reads JSON text and builds new JavaScript values from it. "Parse" means "read text and work out what it says".
JSON.parse always builds new objects and arrays. So when you turn data into JSON text and back, you get a copy. This is called a round trip. For plain data, the copy is a deep copy: nested objects are copied too. But it is not a perfect copy of every value. You'll see what gets lost later in this lesson. The Objects & references lesson explains why a new object is never the same object as the old one.
const user = { name: "Asha", score: 3 };const text = JSON.stringify(user);const copy = JSON.parse(text);console.log(text);console.log(copy.name);Line by line:
- Line 1 makes an object with a name and a score.
- Line 2 turns the object into the text
{"name":"Asha","score":3}. - Line 3 reads that text and builds a new object,
copy. - Line 4 prints the text.
- Line 5 prints
copy.name, which isAsha.
JSON.stringify turns the object into text. JSON.parse reads the text and builds a new object with the same data. The new object is a copy, not the old object.Guess what changes when an object becomes JSON text and is read back. Then step through the real code.
script
item: "keyboard", quantity: 2, shipped: false,}; const text = JSON.stringify(order, null, 2);const copy = JSON.parse(text);console.log(text);console.log(copy.item);JSON.stringify can take two more arguments:
- The second argument can change values before they are written. It is called the replacer, and the next section explains it. When you don't need it, pass
null. - The third argument adds spaces so the text is easier to read.
2means "indent each level by two spaces". Leave it out, or pass0, to get short text on one line.
const value = { name: "Ava", address: { city: "Pune", country: "IN" }, roles: ["admin", "editor"],};{
"name": "Ava",
"address": {
"city": "Pune",
"country": "IN"
},
"roles": [
"admin",
"editor"
]
}Read back{
"name": "Ava",
"address": {
"city": "Pune",
"country": "IN"
},
"roles": [
"admin",
"editor"
]
}- Plain objects and arrays come back the same.
JSON.stringify worked. "Read back" shows what JSON.parse gives you from that text.
Pick each preset in the workbench. Compare the JSON text with what comes back. You will see that:
- A nested user comes back the same.
- A Date comes back as a string.
undefined, functions, and symbols are left out of objects. In arrays, they becomenull.MapandSetbecome empty objects,{}.- A BigInt, or a circular object (an object that points back to itself), throws a
TypeError.
These results come from real JavaScript running in your browser.
JSON text must use double quotes. It can't have a comma after the last item (a trailing comma), and it can't have comments. If the text breaks a rule, JSON.parse throws a SyntaxError. So JSON.parse("{'a':1}") and JSON.parse('{"a":1,}') both throw, and so does text with a comment in it.
const requestBody = JSON.stringify({ title: "Tea", done: false,});const received = JSON.parse(requestBody);console.log(requestBody);console.log(received.done);A request body is the data a program sends to a server. It travels as text. So the sender calls JSON.stringify before sending (line 1). The receiver calls JSON.parse on the text it gets (line 5). Line 6 prints the text, and line 7 prints received.done, which is false.
JSON itself doesn't send or save anything. It only turns data into text and back. Other tools, like fetch, do the sending.
Replacer and reviver
STEP THROUGHA replacer is the second argument to JSON.stringify. It can be a function or an array. A replacer function works like this:
JSON.stringifycalls it once for the whole value. The key is then an empty string,"".- Then it calls it once for each property, with that property's key and value.
- Whatever the function returns is written into the JSON text.
- If it returns
undefinedfor an object property, that property is left out.
Before a parcel leaves the country, a customs officer opens it and checks each item. The officer can take out items that are not allowed, or change the paperwork. Then the parcel goes on its way.
- In real life: The officer checks every item
- In JavaScript: The replacer gets each
keyandvalue - In real life: The officer takes out an item that is not allowed
- In JavaScript: Return
undefinedto leave a property out - In real life: The officer changes the paperwork
- In JavaScript: Return a different value to write instead
Step through a replacer function. Watch it get each key, then see both passwords left out of the JSON text.
script
name: "Ava", password: "rosebud", profile: { password: "also-secret", theme: "dark" },}; const safeText = JSON.stringify(account, (key, value) => { if (key === "password") return undefined; return value;}, 2);console.log(safeText);undefined for password, so that property is left out.A replacer can also be an array of property names, like ["name", "email"]. Then JSON.stringify writes only those properties. This works well for small, flat objects. Use a function when you need a rule, such as "remove every key called password, however deep it is".
const user = { name: "Asha", email: "asha@example.com", role: "admin",};console.log(JSON.stringify(user, ["name", "email"]));Line 6 passes the array ["name", "email"]. The text keeps name and email and leaves out role.
Don't count on this list alone to keep secrets out. Remove secrets on purpose, and check the data before you send it.
A reviver is the second argument to JSON.parse. It is a function. JSON.parse calls it for each value it reads, with the key and the value. Whatever the reviver returns goes into the result instead.
A common use is turning date text back into a Date object. JSON has no date type, so a date travels as text, like "2024-01-01T00:00:00.000Z". This standard way of writing a date is called an ISO string. Dates get their own lesson in the next module. Here you only need to know that JSON stores a Date as text.
A reviver runs while JSON.parse reads the text. Step through to the moment a string becomes a Date object.
script
const user = JSON.parse(text, (key, value) => { if (key === "joined") return new Date(value); return value;}); console.log(user.joined instanceof Date);console.log(user.joined.toISOString());toJSON: an object chooses its own JSON
CODEBefore JSON.stringify writes an object, it checks one thing: does this object have a method called toJSON? If it does, stringify calls that method and writes what it returns, instead of the object.
This is why a Date turns into a string. Every Date already has a toJSON method, Date.prototype.toJSON, and it returns the date as an ISO string.
const price = { cents: 1250, currency: "USD", toJSON() { return "12.50 USD"; },}; console.log(JSON.stringify({ price }));Lines 4 to 6 add a toJSON method that returns the text "12.50 USD". Line 9 turns { price } into JSON. Stringify finds toJSON on price, so it writes "12.50 USD" instead of cents and currency. The printed text is {"price":"12.50 USD"}.
Classes can use toJSON the same way. For example, a Money class may keep the amount in cents, but write "12.50 USD" into JSON. Keep toJSON simple: return plain JSON data. Don't make the result depend on the current time or on variables outside the object. Then the same object always gives the same text.
If an object has toJSON and you also pass a replacer, toJSON runs first. The replacer then gets the value that toJSON returned.
What JSON cannot hold
RECORDED FACTSJSON works well for simple data. Other values change, disappear, or cause an error when you turn them into JSON. The picture shows three of them.
JSON.stringify writes. A Date becomes text, a Map loses its entries, and a BigInt throws an error.The table lists all of them. Each row is one kind of value. The columns show what JSON.stringify prints, what you have after parsing it back, and why. These results were recorded from real JavaScript.
| The value | What stringify prints | What you get back | Why |
|---|---|---|---|
undefined on its own | undefined | Nothing. There is no JSON text. | When the whole value can't be written, stringify returns undefined instead of a string. |
An object property that is undefined | {"a":1} | b is gone. | A property whose value is undefined, a function, or a symbol is left out. |
An array item that is undefined | [1,null] | undefined comes back as null. | An array keeps every place, so a value JSON can't hold is written as null. |
NaN and Infinity | {"n":null,"inf":null} | The numbers come back as null. | JSON has no way to write "not a number" or "infinity". |
| A Date | {"d":"2024-01-01T00:00:00.000Z"} | A string, not a Date. | Date.prototype.toJSON turns the date into an ISO string before it is written. |
| A Map and a Set | {"m":{},"s":{}} | Empty objects. The entries are lost. | They don't keep their entries as normal properties, so stringify can't see them. |
| A BigInt | TypeError | Nothing. Stringify throws. | You have to turn a BigInt into something else yourself, usually a string. |
| A circular object | TypeError | Nothing. Stringify throws. | In JSON text, each object sits inside one parent. An object that points back to itself would make the text go on forever. |
undefined on its own: undefined
An object property that is undefined: {"a":1}
An array item that is undefined: [1,null]
NaN and Infinity: {"n":null,"inf":null}
A Date: {"d":"2024-01-01T00:00:00.000Z"}
A Map and a Set: {"m":{},"s":{}}
A BigInt: TypeError
A circular object: TypeError
"hello"42{ user: { name: "Ava" } }new Date('2024-01-01T00:00:00.000Z'){ name: undefined }{ save() {} }{ score: NaN }new Map([["a", 1]])[1, undefined]{ id: 1n }
Sort each value by what happens when you call JSON.stringify on it and then JSON.parse on the text.
const tags = new Set(["js", "json"]);const text = JSON.stringify([...tags]);const restored = JSON.parse(text);console.log(text);console.log(restored.length);A Set holds unique values, but it doesn't keep them as normal properties. So JSON.stringify can't see them. Line 2 copies the values into an array with [...tags]. Arrays work in JSON, so the text is ["js","json"]. Line 5 prints 2, the length of the parsed array.
const order = { id: 12345678901234567890n };const text = JSON.stringify(order, (_key, value) => typeof value === "bigint" ? value.toString() : value,);const copy = JSON.parse(text);console.log(text);console.log(typeof copy.id);Lines 2 to 4 pass a replacer. When a value is a BigInt, the replacer returns it as a string. So the JSON text holds the digits in quotes, and line 7 prints string. The receiver can check that string and then call BigInt(copy.id) to get the BigInt back.
Don't turn a large ID into a normal number. A normal number can't hold that many digits exactly, so the ID would change.
So a JSON round trip is only a limited way to copy data. It works for plain data. For other values, structuredClone is a better tool. It is a built-in function that also copies Dates, Maps, Sets, and more. The BigInt lesson explains why 1n is a different type from 1. Numbers explains why very large numbers lose their last digits.
Exact digits: context.source and JSON.rawJSON
RECENTNewer JavaScript engines add two tools for big numbers in JSON. An engine is the part of a browser, or of Node, that runs JavaScript.
First, the problem. JSON text can hold a number with many digits, like 12345678901234567890. But a normal JavaScript number keeps only about 16 digits exactly. So JSON.parse rounds the number before your code ever sees it.
The first tool is a third argument for the reviver, called context. For a simple value like a number or a string, context.source is the exact text of that value in the JSON. So the reviver can read the original digits and build a BigInt from them.
const text = '{"id": 12345678901234567890}'; const rounded = JSON.parse(text).id;const safe = JSON.parse(text, (key, value, context) => { if (key === "id") return BigInt(context.source); return value;}).id; console.log(rounded);console.log(safe);checkingcheckingcheckingNewer JavaScript engines can give a reviver the exact text of a value, and can write raw JSON text as it is. Check that these tools exist before you use them in a browser.
In Node 22, JSON.parse('{"id": 12345678901234567890}').id gives the rounded number 12345678901234567000. With context.source, the reviver reads the original digits and makes 12345678901234567890n.
JSON.parse rounds the last ones. A reviver that reads context.source gets the original digits and makes a BigInt.The second tool, JSON.rawJSON, is for writing JSON. JSON.rawJSON("12345678901234567890") makes a special value that holds the exact text you gave it. When JSON.stringify meets that value, it writes the text as it is. So JSON.stringify({ id: JSON.rawJSON("12345678901234567890") }) keeps every digit. Only pass text you trust, and it must be valid JSON.
const id = JSON.rawJSON("12345678901234567890");const text = JSON.stringify({ id });console.log(text);Line 1 makes the raw value. Line 2 turns { id } into JSON, and line 3 prints {"id":12345678901234567890}.
Not every browser has these tools yet, so check before you use them. This is called feature detection:
typeof JSON.rawJSON === "function"tells you whetherJSON.rawJSONexists.JSON.isRawJSON(value)tells you whether a value was made byJSON.rawJSON.- For
context.source, parse a tiny piece of JSON with a reviver and see whethercontext.sourcearrives.
Run these checks in the browser, after the page has loaded. The server that builds the page may support different features. Sending big IDs as strings still works everywhere, so it is the simplest safe choice.
Where you will use this
You use JSON whenever data leaves your program or comes into it. For example, when you:
- Save settings in the browser.
- Save data in a file.
- Send data to a server.
- Read the data a server sends back.
- Write a log without secrets in it.
- Keep sample data for tests.
In Stage 7, the Fetch & JSON lesson uses fetch with response.json(). response.json() follows the same JSON rules you learn here.
Browser storage keeps only text. localStorage is a small store in the browser. It keeps strings, even after the page is closed. To save settings there:
localStorage.setItem("settings", JSON.stringify(settings))saves the settings as one string.JSON.parse(localStorage.getItem("settings") ?? "{}")reads them back.getItemgivesnullwhen nothing is saved, so?? "{}"uses the text of an empty object instead.
The saved text may be broken, or saved by an older version of your page. So be ready for JSON.parse to throw, and show a friendly message if it does.
When you send JSON to a server, keep it short: leave out the third argument. Also tell the server the text is JSON, by setting the request's content type to application/json. Indented JSON is better for files and logs that people read.
The table lists common jobs. Each row shows the job, the tool to use, and one thing to watch out for.
| What you want to do | What to use | What to watch out for |
|---|---|---|
| Save simple settings. | JSON.stringify(settings) | Only save data you can build again later. |
| Read settings from a text file. | JSON.parse(text) | Catch the SyntaxError and show a friendly message. |
| Send data to a server. | JSON.stringify(payload) | Set the JSON content type. Stringify only makes the text; it doesn't send it. |
| Read data a server sends back. | await response.json() | You can't trust it yet. Check that it has the shape you expect. |
| Remove secrets before writing a log. | A replacer function | Return undefined for each property you want left out. |
| Turn text back into a Date or BigInt. | A reviver function | Check the text first, then build the Date or BigInt. |
Common misconceptions
- "JSON is any JavaScript object written as text." No. JSON has stricter rules: double quotes only, no comments, and no comma after the last item.
- "JSON.parse gives me back my original objects." No. It builds new, plain objects and arrays. Methods and classes are gone, unless you rebuild them with a reviver.
- "Dates stay Dates in JSON." No. They become strings, because of
Date.prototype.toJSON. - "A Map turns into JSON like an object." No. A Map doesn't keep its entries as normal properties, so it becomes
{}. Turn it into an array or object first. - "Big numbers are always safe in JSON." No. JSON text can hold many digits, but a JavaScript number keeps only about 16 exactly. Send big numbers as strings, or use
context.sourceand BigInt where it works. - "The replacer and the reviver run at the same time." No. The replacer runs while
JSON.stringifywrites the text. The reviver runs later, whileJSON.parsereads it. - "JSON.parse runs the text as JavaScript code." No. It only reads data. It never runs functions or code. You still need to check the keys and values you get before you use them.
- "JSON.stringify always gives a string." No. If the whole value is
undefined, it returnsundefined. Check the result before you save it or send it. - "If JSON.parse works, the data is safe to use." No. It only proves the text follows the JSON rules. Also check the shape, the allowed values, and the size before you use the data in your app.
The table compares the two functions you can pass in. Each row is one of them.
| Which one | Where you pass it | What it does | Can it remove a property? |
|---|---|---|---|
| Replacer | JSON.stringify(value, replacer) | It changes or removes values before the JSON text is written. | Yes. Return undefined for an object property. |
| Reviver | JSON.parse(text, reviver) | It changes values after they are read, before you get the result. | Yes. Return undefined and the property is deleted. |
Practice exercises
5 EXERCISESDon't run it yet. Type the exact text the program prints.
const data = {
name: "Ava",
score: NaN,
skip: undefined,
tags: ["js", undefined],
};
console.log(JSON.stringify(data));const data = {
name: "Ava",
score: NaN,
skip: undefined,
tags: ["js", undefined],
};
console.log(JSON.stringify(data));The text keeps name. It writes score: NaN as "score":null. It leaves out skip. In the array, it keeps the second place and writes null there.
Write a replacer that removes every property named password. Then check the JSON text.
const user = { name: "Ava", password: "secret", nested: { password: "hidden" } };
const text = JSON.stringify(user, (key, value) => {
if (key === "password") return undefined;
return value;
});
console.log(text);const user = { name: "Ava", password: "secret", nested: { password: "hidden" } };
const text = JSON.stringify(user, (key, value) => {
if (key === "password") return undefined;
return value;
});
console.log(text);Both passwords are object properties, so returning undefined leaves them out. The nested object stays, but it is now empty.
Parse the event so that starts becomes a Date again.
const event = JSON.parse('{"starts":"2024-01-01T00:00:00.000Z"}', (key, value) => {
if (key === "starts") return new Date(value);
return value;
});
console.log(event.starts instanceof Date);
console.log(event.starts.toISOString());const event = JSON.parse('{"starts":"2024-01-01T00:00:00.000Z"}', (key, value) => {
if (key === "starts") return new Date(value);
return value;
});
console.log(event.starts instanceof Date);
console.log(event.starts.toISOString());The reviver swaps the ISO string in starts for a Date. So instanceof Date is true, and toISOString() works.
Add a toJSON method to a class so its JSON text is easy to read.
class Money {
constructor(cents, currency) {
this.cents = cents;
this.currency = currency;
}
toJSON() {
return (this.cents / 100).toFixed(2) + " " + this.currency;
}
}
console.log(JSON.stringify({ price: new Money(1250, "USD") }));class Money {
constructor(cents, currency) {
this.cents = cents;
this.currency = currency;
}
toJSON() {
return (this.cents / 100).toFixed(2) + " " + this.currency;
}
}
console.log(JSON.stringify({ price: new Money(1250, "USD") }));JSON.stringify calls toJSON on the Money object. So the price property gets the string that toJSON returns.
Use context.source so the big ID keeps all its digits.
const text = '{"id": 12345678901234567890}';
const safe = JSON.parse(text, (key, value, context) => {
if (key === "id") return BigInt(context.source);
return value;
});
console.log(safe.id);const text = '{"id": 12345678901234567890}';
const safe = JSON.parse(text, (key, value, context) => {
if (key === "id") return BigInt(context.source);
return value;
});
console.log(safe.id);context.source is the original text of the number, read before it was rounded. So BigInt gets every digit.
Check your understanding
8 QUESTIONSQuestion 1 of 8What does JSON.stringify do?
Choose an answer to see the explanation.
Question 2 of 8An object property is undefined. What does this print?
Read the code, then predictconsole.log(JSON.stringify({ a: 1, b: undefined, c: null }));Choose an answer to see the explanation.
Question 3 of 8An array holds values JSON can't hold. What does this print?
Read the code, then predictconsole.log(JSON.stringify([1, undefined, NaN, Infinity]));Choose an answer to see the explanation.
Question 4 of 8Why might you pass a replacer function to JSON.stringify?
Choose an answer to see the explanation.
Question 5 of 8A date comes back from JSON. What does this print?
Read the code, then predictconst obj = JSON.parse('{"d":"2024-01-01T00:00:00.000Z"}'); console.log(obj.d instanceof Date);Choose an answer to see the explanation.
Question 6 of 8An object has a toJSON method. What does this print?
Read the code, then predictconst item = { value: 7, toJSON() { return "seven"; } }; console.log(JSON.stringify({ item }));Choose an answer to see the explanation.
Question 7 of 8Which text is not valid JSON?
Choose an answer to see the explanation.
Question 8 of 8What problem does the reviver's context.source solve?
Choose an answer to see the explanation.
Key takeaways
JSON.stringifyturns plain data into JSON text.JSON.parsereads JSON text and builds new values.- A replacer changes or removes values while the text is written. A reviver changes values while the text is read.
toJSONlets an object choose what it looks like in JSON.- JSON can't hold functions,
undefined, symbols, BigInt, circular objects, classes, or Dates as Dates. A parsed object is always a new object. context.sourceandJSON.rawJSONkeep exact digits in newer engines. Check that they exist before you use them.
Final definition: JSON is a strict text format for simple data that almost every program can read. A replacer and a reviver let you change what goes out and what comes back.
Up next: Choosing a data structure.