JSON 05 ๐ Parsing JSON and Stringifying Data in JavaScript/Node.js
The JSON object is the boundary between the text and the in-memory data in JavaScript. The JSON.parse() method takes a JSON string and returns the JavaScript value, and the JSON.stringify() method takes a JavaScript value and returns the JSON string . The two methods are the built-in serialization mechanism, and they are the most frequently used APIs in the web development. The surface is smallโtwo functions and a handful of parametersโbut the behavior at the edges is where the bugs live. The circular references throw, the Date objects serialize through the toJSON() hook, the undefined and the functions are dropped, the NaN becomes null, and the large integers are rounded before the reviver is even called.
The JSON.parse() accepts the text and the optional reviver. The reviver is called for every key-value pair, bottom-up, and its return value replaces the value in the result. The reviver is the transformation hook, and it is also the security surface: the Axios CVE-2026-42044 showed that a polluted Object.prototype.parseReviver is invoked for every JSON response, and the attacker can silently escalate a role: "user" to role: "admin" without any error . The JSON.stringify() accepts the value, the optional replacer, and the optional space. The replacer can filter or transform the properties, and the space controls the indentation. The toJSON() method on an object is called before the serialization, which is how the Date becomes an ISO string .
Key point: JSON.parse(text, reviver) parses the text and applies the reviver bottom-up. JSON.stringify(value, replacer, space) serializes the value and applies the replacer. The reviver is a security surface, and the large integers are rounded before the reviver sees them. The circular references throw, and the toJSON() hook is the custom serialization mechanism.
Why the JavaScript implementation has the edges
The single-thread problem. Node.js runs the application on a single main thread, and that thread also runs the event loop. When the code calls JSON.stringify() or JSON.parse() on a large object, the work runs to completion on the same thread. While the serialization or the parsing is running, the new requests are accepted by the operating system but the JavaScript cannot handle them yet. The timers fire late, the health checks time out, and the event loop lag climbs into the hundreds of milliseconds . The edge is the synchronous blocking, and the fix is to move the large payloads off the main thread or to use the streaming parsers.
The circular reference problem. A JavaScript object can contain a reference to itself, directly or through a chain. The JSON.stringify() cannot represent the cycle, and it throws the TypeError: Converting circular structure to JSON . The fix is the WeakSet in the replacer, which records the objects that have been visited and returns a marker string for the ones that have already been seen. The WeakSet does not prevent the garbage collection, which is the reason it is the correct data structure for the one-time serialization .
The large integer problem. The JSON.parse() converts every number to the IEEE-754 double, and the integers above Number.MAX_SAFE_INTEGER (2^53 – 1) are rounded. The 9453860324986718414640 becomes 9453860324986719305728, which is off by 891,088 . The reviver cannot recover the lost precision because the value it receives is already the rounded double. The fix is to serialize the large integers as strings, or to use the parser that preserves the precision, such as the @decentralchain/parse-json-bignumber library, which returns the numbers with more than 15 digits as strings by default .
The toJSON priority problem. The JSON.stringify() calls the toJSON() method on the object before the serialization, and the return value of the toJSON() is what the replacer receives. The Date object has the toJSON() that returns the ISO string, which is why the date serializes as a string . The custom toJSON() is the mechanism for the custom serialization, but it also means the replacer cannot see the original objectโit sees the toJSON() result.
The eval comparison problem. The JSON.parse() is the safe alternative to the eval(). The eval() executes the text as JavaScript, and a malicious text can contain the arbitrary code. The JSON.parse() validates the grammar and rejects the text that is not valid JSON . The distinction is the reason the JSON.parse() is the standard, and the eval() is the anti-pattern.
a. The JSON.parse() and the reviver mechanics
The JSON.parse() signature is JSON.parse(text, reviver). The text is the string, and the reviver is the optional function. The function receives the key and the value, and the return value is the value that the result uses. The function is called bottom-up: the leaf values are processed first, and the container values are processed after their children .
// The basic parse
const data = JSON.parse('{"name":"Ada","age":36}');
// { name: 'Ada', age: 36 }
// The reviver transforms the values
const parsed = JSON.parse('{"name":"Ada","age":36}', (key, value) => {
// The function is called for every key-value pair.
// The key is the property name, the value is the parsed value.
if (key === 'age') return value * 2;
return value;
});
// { name: 'Ada', age: 72 }
The bottom-up order is the mechanism that allows the reviver to transform the children before the parent. The parent’s value is the object with the transformed children, and the reviver can inspect the transformed children.
// The bottom-up order
const parsed = JSON.parse('{"a":{"b":1}}', (key, value) => {
console.log(key, value);
return value;
});
// b 1
// a { b: 1 }
// '' { a: { b: 1 } }
// The root key is the empty string.
// The root value is the whole parsed object.
The root call has the empty string as the key and the whole parsed value as the value. The reviver can return a different value for the root, and the JSON.parse() returns the transformed root .
b. The JSON.stringify() and the replacer mechanics
The JSON.stringify() signature is JSON.stringify(value, replacer, space). The value is the object, the replacer is the optional function or array, and the space is the optional indentation . The replacer function receives the key and the value, and the return value is the value that the serialization uses. Returning undefined omits the property from the output .
// The basic stringify
const json = JSON.stringify({ name: "Ada", age: 36 });
// '{"name":"Ada","age":36}'
// The replacer function
const filtered = JSON.stringify({ name: "Ada", password: "x" }, (key, value) => {
// Returning undefined omits the property.
if (key === 'password') return undefined;
return value;
});
// '{"name":"Ada"}'
// The space parameter
const pretty = JSON.stringify({ name: "Ada" }, null, 2);
// '{\n "name": "Ada"\n}'
The replacer array is the whitelist form. It only keeps the specified keys, and the filtering is recursive for the nested objects . The replacer function is the flexible form, and it can inspect every key-value pair.
The toJSON() method is the custom serialization hook. The JSON.stringify() calls it before the serialization, and the return value is what the replacer receives. The Date object uses it to serialize as the ISO string, and a custom class can use it to control its own serialization .
// The toJSON hook
class User {
constructor(name) {
this.name = name;
}
toJSON() {
// The return value is what the serialization uses.
return { userName: this.name, type: 'user' };
}
}
JSON.stringify(new User('Ada'));
// '{"userName":"Ada","type":"user"}'
c. The security and the performance edges
The security edge is the reviver. The JSON.parse(text, this.parseReviver) call is the pattern that the Axios library used, and the this.parseReviver walks the prototype chain. The polluted Object.prototype.parseReviver is found, and the attacker-controlled function is called for every key-value pair in every JSON response . The fix is the own-property check: the reviver must be an explicit function, not a function inherited from the prototype chain.
// The unsafe pattern (the Axios CVE)
// const data = JSON.parse(text, this.parseReviver);
// The this.parseReviver may be inherited from the prototype chain.
// The safe pattern
function safeParse(text, reviver) {
// The reviver must be an explicit function.
if (typeof reviver !== 'function') {
return JSON.parse(text);
}
return JSON.parse(text, reviver);
}
The performance edge is the synchronous blocking. The JSON.stringify() and the JSON.parse() run to completion on the main thread, and the large payloads block the event loop . The fix is to move the large payloads off the main thread, to use the streaming parsers, or to split the work into the smaller chunks.
// The safe stringify with the circular reference handling
function safeStringify(obj, space = 2) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
}
return value;
}, space);
}
The WeakSet records the objects that have been visited, and the circular reference is replaced with the [Circular] marker instead of throwing . The safeStringify is the pattern for the logging and the debugging, where the circular references are common and the serialization must not crash the application.
Complete Example Session
// ============================================
// PART 1: THE BASIC PARSE
// ============================================
const data = JSON.parse('{"name":"Ada","age":36}');
// { name: 'Ada', age: 36 }
// The text is validated against the grammar.
// The valid JSON is parsed, the invalid is rejected.
// ============================================
// PART 2: THE REVIVER
// ============================================
const parsed = JSON.parse('{"name":"Ada","age":36}', (key, value) => {
if (key === 'age') return value * 2;
return value;
});
// { name: 'Ada', age: 72 }
// The reviver is called for every key-value pair.
// The return value replaces the value.
// ============================================
// PART 3: THE BOTTOM-UP ORDER
// ============================================
JSON.parse('{"a":{"b":1}}', (key, value) => {
console.log(key, value);
return value;
});
// b 1
// a { b: 1 }
// '' { a: { b: 1 } }
// The leaf is processed first.
// The root is processed last.
// ============================================
// PART 4: THE PROTOTYPE POLLUTION
// ============================================
// The polluted reviver
Object.prototype.parseReviver = (key, value) => {
if (key === 'role') return 'admin';
return value;
};
const response = JSON.parse('{"role":"user","balance":100}');
// { role: 'admin', balance: 100 } (the role was escalated)
delete Object.prototype.parseReviver;
// The reviver came from the prototype chain.
// The data was silently modified.
// ============================================
// PART 5: THE SAFE PATTERN
// ============================================
function safeParse(text, reviver) {
if (typeof reviver !== 'function') {
return JSON.parse(text);
}
return JSON.parse(text, reviver);
}
// The reviver must be the explicit function.
// The inherited reviver is not used.
// ============================================
// PART 6: THE LARGE INTEGER
// ============================================
const big = JSON.parse('{"id":9453860324986718414640}');
console.log(big.id); // 9453860324986719305728 (rounded)
// The precision is lost before the reviver is called.
// The double has 53 bits of integer precision.
// ============================================
// PART 7: THE STRING WORKAROUND
// ============================================
const safeBig = JSON.parse('{"id":"9453860324986718414640"}');
console.log(safeBig.id); // "9453860324986718414640" (exact)
// The large integer is serialized as a string.
// The string preserves the exact digits.
// ============================================
// PART 8: THE TOJSON HOOK
// ============================================
class User {
constructor(name) {
this.name = name;
}
toJSON() {
return { userName: this.name, type: 'user' };
}
}
JSON.stringify(new User('Ada'));
// '{"userName":"Ada","type":"user"}'
// The toJSON is called before the serialization.
// The return value is what the serialization uses.
// ============================================
// PART 9: THE CIRCULAR REFERENCE
// ============================================
const user = { name: 'Ada', profile: {} };
user.profile.owner = user;
try {
JSON.stringify(user);
} catch (e) {
// TypeError: Converting circular structure to JSON
}
// The circular reference throws.
// The fix is the WeakSet in the replacer.
// ============================================
// PART 10: THE CHECKLIST
// ============================================
// [ ] The JSON.parse is used, not the eval.
// [ ] The reviver is the explicit function, not the inherited one.
// [ ] The large integers are serialized as strings.
// [ ] The toJSON hook is used for the custom serialization.
// [ ] The circular references are handled with the WeakSet.
// [ ] The large payloads are moved off the main thread.
The ten parts covered the basic parse, the reviver, the bottom-up order, the prototype pollution, the safe pattern, the large integer, the string workaround, the toJSON hook, the circular reference, and the checklist. The through-line is that the JSON.parse() and JSON.stringify() are the boundary, and the edges are the security and the precision.
Quick Reference
The JSON.parse() signature
| Parameter | Type | Purpose |
|---|---|---|
text | string | The JSON text to parse |
reviver | function | The optional transformation hook |
| Return | any | The parsed value |
The JSON.stringify() signature
| Parameter | Type | Purpose |
|---|---|---|
value | any | The value to serialize |
replacer | function | array | The optional filter or transform |
space | number | string | The optional indentation |
| Return | string | The JSON text |
The edge cases
| Case | Behavior |
|---|---|
| Circular reference | Throws TypeError |
Date | Serialized via toJSON() as ISO string |
undefined | Omitted from the output |
NaN / Infinity | Converted to null |
| Large integer | Rounded to the nearest double |
BigInt | Throws TypeError |
The security patterns
| Pattern | Safe? | Reason |
|---|---|---|
JSON.parse(text) | Yes | No reviver, no attack surface |
JSON.parse(text, explicitReviver) | Yes | The reviver is the explicit function |
JSON.parse(text, this.reviver) | No | The reviver may be inherited |
eval('(' + text + ')') | No | The text is executed |
Best Practices
โ Do This:
// Use JSON.parse, not eval
const data = JSON.parse(text); // โ
// Use the explicit reviver function
const parsed = JSON.parse(text, (key, value) => value); // โ
// Serialize the large integers as strings
const safe = JSON.stringify({ id: "9453860324986718414640" }); // โ
// Use the WeakSet for the circular references
const seen = new WeakSet(); // โ
โ Don’t Do This:
// Use eval to parse
const data = eval('(' + text + ')'); // โ
// Use the inherited reviver
const parsed = JSON.parse(text, this.parseReviver); // โ
// Assume the large integers are exact
JSON.parse('{"id":9453860324986718414640}'); // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The role is escalated | The reviver is inherited from the prototype | Use the own-property check |
| The large integer is rounded | The IEEE-754 double precision | Serialize as a string |
| The serialization throws | The circular reference | Use the WeakSet in the replacer |
| The event loop is blocked | The large payload on the main thread | Move the work off the main thread |
| The date is a string | The toJSON() hook | Convert back to the Date after the parse |
Real-World Examples
1. The basic parse
JSON.parse('{"name":"Ada"}');
2. The reviver
JSON.parse('{"age":36}', (k, v) => k === 'age' ? v * 2 : v);
3. The bottom-up order
JSON.parse('{"a":{"b":1}}', (k, v) => { console.log(k); return v; });
4. The prototype pollution
Object.prototype.parseReviver = (k, v) => k === 'role' ? 'admin' : v;
5. The large integer
JSON.parse('{"id":9453860324986718414640}').id; // rounded
6. The string workaround
JSON.parse('{"id":"9453860324986718414640"}').id; // exact
7. The toJSON hook
class User { toJSON() { return { name: 'Ada' }; } }
8. The circular reference
const seen = new WeakSet();
JSON.stringify(obj, (k, v) => seen.has(v) ? '[Circular]' : v);
9. The safe parse
function safeParse(text, reviver) { return JSON.parse(text, typeof reviver === 'function' ? reviver : undefined); }
10. The event loop blocking
// The large payload on the main thread blocks the event loop.
Visual
The Parse and the Reviver
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE PARSE PIPELINE โ
โ โ
โ JSON text โ
โ '{"name":"Ada","age":36}' โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The tokenizer โ Validates the grammar โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The parser โ Constructs the values โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The reviver โ Transforms the values โ
โ โ (bottom-up) โ โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ { name: "Ada", โ The result โ
โ โ age: 36 } โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Prototype Pollution
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE INHERITED REVIVER โ
โ โ
โ Object.prototype โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ parseReviver: fn โ The polluted function โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โ inherited by โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ config โ The Axios config object โ
โ โ (no own parseReviverโ โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ JSON.parse(text, โ The inherited reviver is used โ
โ โ this.parseReviver)โ โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ role: "admin" โ The data is silently modified โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Large Integer Precision
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE DOUBLE PRECISION โ
โ โ
โ The text: โ
โ {"id":9453860324986718414640} โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ JSON.parse โ The number is the double โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ 9453860324986719305728โ The precision is lost โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ The fix: serialize as a string or use a lossless parser. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
JSON.parse | The safe parser, no eval |
reviver | The transformation hook, called bottom-up |
| The root call | The key is '', the value is the whole object |
| The prototype pollution | The inherited reviver can modify the data |
| The large integer | The IEEE-754 double rounds above 2^53 |
| The safe pattern | The own-property check for the reviver |
| The circular reference | Throws TypeError, fix with the WeakSet |
The toJSON hook | The custom serialization mechanism |
Key takeaways:
JSON.parseis the safe alternative toeval. The parser validates the grammar and rejects the text that is not valid JSON, while theevalexecutes the text as JavaScript . TheJSON.parsedoes not execute the text; it parses it.- The reviver is the transformation hook and the security surface. The function is called for every key-value pair, bottom-up, and the return value replaces the value . The polluted
Object.prototype.parseReviveris invoked for every JSON response, and the Axios CVE-2026-42044 is the concrete example: therole: "user"was silently escalated torole: "admin". - The large integers are rounded before the reviver is called. The
JSON.parseconverts every number to the IEEE-754 double, and the integers above 2^53 are rounded . The fix is to serialize the large integers as strings, or to use the parser that preserves the precision . - The
JSON.stringifyhas thetoJSONhook and the circular reference edge. ThetoJSON()method is called before the serialization, which is how theDatebecomes an ISO string . The circular reference throws, and the fix is theWeakSetin the replacer . - The synchronous blocking is the Node.js performance edge. The
JSON.stringify()and theJSON.parse()run to completion on the main thread, and the large payloads block the event loop . The fix is to move the work off the main thread or to use the streaming parsers.
Remember: The JSON.parse() and JSON.stringify() are the boundary between the text and the data. The JSON.parse() is the safe parser, and the reviver is the transformation hook and the security surface. The JSON.stringify() has the toJSON hook and the replacer, and the circular reference is the edge. The large integers are the precision hazard, and the synchronous blocking is the performance edge. The two methods are the most used APIs in the JavaScript, and the edges are where the bugs live.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!