JSON
Send and store data as text with JSON.stringify, JSON.parse, replacers, revivers, toJSON, and the newest source-text tools.
- 01Pack and unpack dataUse JSON.stringify and JSON.parse on plain objects and arrays.
- 02Customize the tripFilter with replacers, restore richer values with revivers, and write toJSON methods.
- 03Spot lossy edgesPredict what JSON drops, changes, or refuses, including recent raw JSON features.
JSON as data text
JSON (JavaScript Object Notation) is a small text format for data. It can describe strings, finite numbers, booleans, null, arrays, and plain objects with double-quoted property names. That is why the curriculum summary says JSON helps you send and store data as text.
JSON looks like JavaScript object and array literals, but it is stricter and smaller. It has no comments, no single-quoted strings, no functions, no undefined, no symbols, no BigInt, and no special Date value. The reward for those limits is portability: almost every programming language can read it.
Imagine sending a package through many countries. The shipping label needs a common, simple language: name, address, weight, yes/no checkboxes. JSON plays that role for programs. It is boring on purpose so other systems can read it without knowing JavaScript.
- In real life: Packing an object into a label
- In JavaScript:
JSON.stringify(value)creates JSON text - In real life: Unpacking on arrival
- In JavaScript:
JSON.parse(text)creates JavaScript data - In real life: Simple boxes and lists
- In JavaScript: Objects and arrays
- In real life: Words, counts, yes/no, empty
- In JavaScript: strings, numbers, booleans, and
null
Where the analogy stops: A shipping label cannot carry the item itself. JSON can describe data, but not functions, object identity, prototypes, private fields, or circular references.
You have already met objects and references, numbers, BigInt, and symbols. This lesson shows how those values behave when they cross the JSON border. Later, the Fetch & JSON lesson in Stage 7 will use the same ideas to read responses from servers.
JSON.stringify and JSON.parse
INTERACTIVEJSON.stringify(value) packs a supported JavaScript value into JSON text. JSON.parse(text) reads JSON text and returns fresh JavaScript values. Fresh matters: a JSON round trip can act like a deep copy for plain data, but the Objects & references lesson’s warning still applies: it is not a perfect clone.
Predict what changes when an object is packed into JSON text and unpacked again. Then step through the real operations.
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);The second argument to JSON.stringify can customize values. The third argument controls pretty-printing. Passing 2 means two spaces; passing 0 or omitting it gives compact JSON.
const value = { name: "Ava", address: { city: "Pune", country: "IN" }, roles: ["admin", "editor"],};{
"name": "Ava",
"address": {
"city": "Pune",
"country": "IN"
},
"roles": [
"admin",
"editor"
]
}Parsed back{
"name": "Ava",
"address": {
"city": "Pune",
"country": "IN"
},
"roles": [
"admin",
"editor"
]
}- Plain objects and arrays round-trip clearly.
JSON.stringify succeeded. Parse below shows what comes back after the round trip.
Try each preset. A nested user survives; a Date comes back as text; undefined, functions, and symbols disappear or become null; Map and Set become empty objects; BigInt and circular references throw TypeError. Those are real browser results, not hand-written examples.
Valid JSON uses double quotes and has no trailing commas or comments. JSON.parse("{'a':1}"), JSON.parse('{"a":1,}'), and commented text all throw SyntaxError.
Replacer and reviver
STEP THROUGHA replacer is a function or an array passed to JSON.stringify. A function replacer is called for the wrapper and each property; returning undefined removes an object property. Think of it as a customs officer checking every item before shipping.
Before a parcel leaves, customs can remove unsafe items or rewrite paperwork. A replacer gives you that checkpoint while JSON is being made.
- In real life: Inspect every item
- In JavaScript: The replacer receives each
keyandvalue - In real life: Confiscate passwords
- In JavaScript: Return
undefinedfor sensitive properties - In real life: Relabel an item
- In JavaScript: Return a different serializable value
Where the analogy stops: Real customs rules know the whole shipment. A replacer only sees one key and value at a time, with this as the holder object.
Step through a replacer function. Watch every key visit, then see passwords disappear from the final JSON.
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);A replacer array is simpler: JSON.stringify(user, ["name", "email"]) whitelists those property names. It is useful for small, flat output, but a function is clearer when you need rules such as “remove every password key.”
A reviver is the matching hook for JSON.parse. It receives each parsed key and value and can return a replacement. A common use is restoring an ISO date string into a Date object. Date basics are in the next module, so here the key idea is only that JSON stores the Date as text.
Revivers run while parsing. Step through the point where a plain 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’s own packing instructions
CODEBefore JSON.stringify writes an object, it checks for a toJSON method. If the method exists, stringify serializes the method’s return value instead of the original object. That is why Dates become ISO strings: Date.prototype.toJSON already exists.
const price = { cents: 1250, currency: "USD", toJSON() { return "12.50 USD"; },}; console.log(JSON.stringify({ price }));A class can use this to publish a deliberate data shape. For example, a money object may store cents internally but ship as "12.50 USD". Keep toJSON boring and predictable: it should return JSON-friendly data, not depend on the current time or hidden global state.
toJSON runs before the replacer sees that value. If both exist, the replacer receives the value returned by toJSON.
What JSON cannot hold
RECORDED FACTSJSON is excellent for simple data, but it is lossy outside that lane. Here are the edge cases recorded from real JavaScript behavior.
| Value | Example output | Round trip | Why |
|---|---|---|---|
Object property undefined | {"a":1} | b is gone | Object properties whose value is undefined, a function, or a symbol are omitted. |
Array slot undefined | [1,null] | undefined becomes null | Arrays keep their length, so unsupported entries become null. |
NaN and infinities | {"n":null,"inf":null} | Numbers become null | JSON has no spelling for not-a-number or infinity. |
| Date | {"d":"2024-01-01T00:00:00.000Z"} | Date becomes string | Date.prototype.toJSON returns the ISO string before stringify writes it. |
| Map and Set | {"m":{},"s":{}} | Entries are lost | Their data is not stored in enumerable string-keyed properties. |
| BigInt | TypeError | Nothing is produced | BigInt must be converted deliberately, usually to a string. |
| Circular reference | TypeError | Nothing is produced | JSON is a tree. An object pointing back to itself is a graph. |
Object property undefined: {"a":1}
Array slot undefined: [1,null]
NaN and infinities: {"n":null,"inf":null}
Date: {"d":"2024-01-01T00:00:00.000Z"}
Map and Set: {"m":{},"s":{}}
BigInt: TypeError
Circular reference: 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 after JSON.stringify followed by JSON.parse.
This is why a JSON round trip is only a limited deep-copy trick. It can copy plain data, but structuredClone is the more honest tool for many JavaScript values. The BigInt lesson explains why 1n is not the same type as 1, and Numbers explains why huge numeric IDs can lose precision.
Source text access and JSON.rawJSON
RECENTRecent engines add two tools for advanced JSON work. First, a reviver can receive a third context argument for primitive values. context.source is the exact JSON source text for that value. That matters for huge numbers because JSON.parse normally converts them to Number before your code sees 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);checkingcheckingchecking support after mountRecent JavaScript engines can expose the original source text to a reviver and can insert trusted raw JSON. Feature-detect before using these in browsers.
In Node 22, JSON.parse('{"id": 12345678901234567890}').id is the rounded number 12345678901234567000. With context.source, the reviver can read the original digits and create 12345678901234567890n. Second, JSON.rawJSON("12345678901234567890") creates a trusted raw JSON value so JSON.stringify({ id: JSON.rawJSON("12345678901234567890") }) outputs a raw number token.
const id = JSON.rawJSON("12345678901234567890");const text = JSON.stringify({ id });console.log(text);Feature-detect in browsers after mount with typeof JSON.rawJSON === "function", JSON.isRawJSON, and a small parse test for context.source. These additions are useful, but strings remain the simplest cross-platform way to move big IDs safely.
Where you will use this
JSON appears anywhere data crosses a boundary: saving settings in storage, putting data into a file, sending request bodies, reading API responses, logging safe snapshots, and writing tests with fixture data. Stage 7’s Fetch & JSON lesson will combine fetch with response.json(); the JSON rules here are the same rules underneath.
| Job | Tool | Watch out for |
|---|---|---|
| Save simple preferences | JSON.stringify(settings) | Only store data you can rebuild later. |
| Read configuration text | JSON.parse(text) | Handle SyntaxError and show a friendly message. |
| Remove secrets before logging | Replacer function | Return undefined for object properties you do not want. |
| Restore known richer values | Reviver function | Validate strings before turning them into Dates or BigInts. |
Common misconceptions
- “JSON is any JavaScript object text.” No. JSON has stricter grammar: double quotes, no comments, no trailing commas.
- “JSON.parse restores the original objects.” It creates fresh plain objects and arrays. Prototypes and methods are gone unless you revive or rebuild them.
- “Dates survive JSON.” They become strings through
Date.prototype.toJSON. - “Map serializes like an object.” A Map’s entries are not enumerable string-keyed properties, so it becomes
{}unless you convert it. - “Big numbers are always safe in JSON.” JSON text may contain many digits, but JavaScript Number precision is limited. Use strings or
context.sourceplus BigInt when available. - “A replacer and reviver run at the same time.” Replacer runs while stringifying; reviver runs later while parsing.
| Hook | Used by | Main job | Can remove? |
|---|---|---|---|
| Replacer | JSON.stringify(value, replacer) | Change or filter values before JSON text is produced | Yes: return undefined for object properties |
| Reviver | JSON.parse(text, reviver) | Change parsed values before the final result is returned | Yes: return undefined to delete a property from its holder |
Practice exercises
5 EXERCISESWithout running it first, type the exact string printed by the program.
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 object keeps name, converts score: NaN to score:null, drops skip, and keeps the array slot by writing null.
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 password properties are object properties, so returning undefined removes them. The nested object remains, now empty.
Parse the event so 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 replaces the ISO string for starts with a Date. That makes instanceof Date true and allows toISOString().
Add packing instructions to a class-like value so JSON text is friendly.
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 the Money instance’s toJSON, so the price property gets the returned string.
Use context.source to avoid Number precision loss.
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);The reviver sees the original primitive source text before you choose the replacement, so BigInt receives the exact digits.
Check your understanding
8 QUESTIONSQuestion 1 of 8What does JSON.stringify do?
Choose an answer to see the explanation.
Question 2 of 8What does output question 1 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 8What does output question 2 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 8What does output question 3 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 8What does output question 4 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 invalid JSON?
Choose an answer to see the explanation.
Question 8 of 8What problem can reviver context.source solve?
Choose an answer to see the explanation.
Key takeaways
JSON.stringifypacks supported data into text;JSON.parseunpacks valid JSON text.- Replacers filter or transform before shipping; revivers rebuild selected values while receiving.
toJSONlets an object choose its own JSON-friendly representation.- JSON cannot hold functions,
undefined, symbols, BigInt, circular references, object identity, prototypes, or Date objects as Dates. - Recent
context.sourceandJSON.rawJSONAPIs help with exact source text, but feature-detect them.
Final definition: JSON is a strict, portable text format for simple data, with hooks for carefully changing what leaves and what comes back.
Up next: Choosing a data structure.