The method JSON.stringify() is the built-in JavaScript method that turns a value, most often an object or an array, into a string of JSON text. We need it whenever data leaves the program: an API request body, a localStorage entry and a log line all carry text, not live objects. The method follows fixed rules. Values that JSON can represent are written as they are. Some values become null or are left out, and a few make the call throw a TypeError. Two optional arguments, the replacer and the space value, control which data goes out and how the text is laid out.
This page first shows how to call the method and what it returns. It then lists the values that change or disappear on the way to text, with the exact errors Node.js 22 throws for the ones it refuses. After that come the replacer, indentation and the toJSON() hook. The last part covers everyday uses such as request bodies, deep copies and HTML pages, with the trap in each one. Reading JSON text back into values is the job of JSON.parse().
1. How to Call JSON.stringify()
The method takes the value to convert plus two optional arguments, as listed in the MDN reference.
JSON.stringify(value)
JSON.stringify(value, replacer)
JSON.stringify(value, replacer, space)
- value is the data to convert: an object, an array or a single value.
- replacer is either a function that can change or drop values, or an array of property names to keep. Pass null to skip it.
- space adds line breaks and indentation. A number sets the count of spaces, and a string such as a tab character is used as the indent.
Without the optional arguments, the method returns compact text with no spaces. Node.js 22 prints:
const order = { id: 7, items: ["pen", "ink"], paid: true, coupon: null };
console.log(JSON.stringify(order));
console.log(typeof JSON.stringify(order));
console.log(JSON.stringify("hi"), JSON.stringify(42), JSON.stringify(null));
{"id":7,"items":["pen","ink"],"paid":true,"coupon":null}
string
"hi" 42 null
The result is a string in all three lines. Note the double quotes around “hi”: the output is the JSON text for a string, quotes included. The only case where the method returns something other than a string is when the value itself cannot become JSON, such as undefined or a function. In that case, the method returns undefined.
2. Values That Change or Disappear
JSON has six kinds of values, and JavaScript has many more. When JSON.stringify() finds a value with no JSON form, it does not throw in most cases. Instead, it writes null or leaves the value out. We can see each case in one object that mixes most of the problem types:
const value = {
name: "Lokesh",
nickname: undefined,
greet() { return "hi"; },
tags: ["a", undefined, () => 1],
score: NaN,
limit: Infinity,
joined: new Date(Date.UTC(2026, 9, 2, 10, 15)),
roles: new Set(["admin"]),
prefs: new Map([["theme", "dark"]]),
pattern: /ab+c/
};
console.log(JSON.stringify(value, null, 2));
{
"name": "Lokesh",
"tags": [
"a",
null,
null
],
"score": null,
"limit": null,
"joined": "2026-10-02T10:15:00.000Z",
"roles": {},
"prefs": {},
"pattern": {}
}
The property nickname and the method greet are gone. Inside the array, the same kinds of values became null, because removing them would shift the positions of the other items. The numbers NaN and Infinity became null, the date became an ISO 8601 string, and the Set, Map and regular expression became empty objects. The figure shows the order of the checks and the result for each type.

The dropped properties cause the most bugs, because no error or warning reports them. A client that receives no nickname field may treat it as “not set”, whereas a null may mean “cleared”. The JSON data types page explains which values exist on the JSON side.
2.1. BigInt and Circular References Throw
Two cases do stop the call with a TypeError. A BigInt has no JSON form, and an object that refers to itself would produce endless text. Node.js 22 reports them like this:
TypeError: Do not know how to serialize a BigInt
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
--- property 'self' closes the circle
The second message names the property that closes the loop, which helps a lot in large objects. Circular structures often come from ORM entities that link a parent and its children, or from framework objects such as an HTTP request. Copy only the fields we need into a plain object before converting it.
2.2. Converting Map and Set by Hand
A Map or Set turns into {} because its entries are not ordinary properties. Convert it first: spread a Set into an array, and use Object.fromEntries() for a Map whose keys are strings.
const roles = new Set(["admin", "editor"]);
const prefs = new Map([["theme", "dark"], ["lang", "en"]]);
console.log(JSON.stringify({ roles: [...roles], prefs: Object.fromEntries(prefs) }));
{"roles":["admin","editor"],"prefs":{"theme":"dark","lang":"en"}}
3. Choosing What Goes Out with a Replacer
The second argument decides which data ends up in the text. It comes in two forms, and each suits a different job.
3.1. An Array of Property Names
An array works as an allow-list. Only the listed names are written, at every level of nesting. That makes it a quick way to keep a password or an email address out of the output:
const user = { id: 1, name: "Lokesh", email: "lokesh@example.com", password: "secret" };
console.log(JSON.stringify(user, ["id", "name"]));
const post = { id: 1, name: "A", owner: { id: 2, name: "B", email: "x" } };
console.log(JSON.stringify(post, ["id", "owner"]));
{"id":1,"name":"Lokesh"}
{"id":1,"owner":{"id":2}}
The second line shows the catch. The list applies to nested objects too, so name disappeared from owner as well. Every name needed at any depth must be in the list.
3.2. A Function That Rewrites Values
A replacer function receives each key and value and returns what should be written instead. Returning undefined drops the property. This version masks an account number for a log message and keeps the last four digits:
const account = { name: "Lokesh", accountNumber: "3044444444", city: "New York" };
function maskInfo(key, value) {
if (key === "accountNumber" && typeof value === "string") {
return value.length > 4 ? "****" + value.slice(-4) : "****";
}
return value;
}
console.log(JSON.stringify(account, maskInfo, 2));
{
"name": "Lokesh",
"accountNumber": "****4444",
"city": "New York"
}
The function must return the original value for every other key. If it returns nothing for a key, that property is missing from the output.
The replacer runs from the top down. Its first call receives the whole value with an empty key, then each property in turn, parents before children. This order is the reverse of the reviver in JSON.parse(), which starts with the deepest values:
const seen = [];
JSON.stringify({ a: 1, b: { c: 2 } }, function (key, value) {
seen.push(JSON.stringify(key));
return value;
});
console.log(seen.join(" "));
"" "a" "b" "c"
3.3. Writing BigInt Values
A replacer is also the clean way around the BigInt error. Converting the value to a string is the portable choice, and it is what most APIs expect for large ids. In newer engines, JSON.rawJSON() writes the digits as a real JSON number instead. Per MDN, Node.js 21, Chrome 114, Firefox 135 and Safari 18.4 or newer support it.
const data = { id: 9007199254740993n, total: 12.5 };
console.log(JSON.stringify(data, (key, value) =>
typeof value === "bigint" ? value.toString() : value));
console.log(JSON.stringify(data, (key, value) =>
typeof value === "bigint" ? JSON.rawJSON(value.toString()) : value));
{"id":"9007199254740993","total":12.5}
{"id":9007199254740993,"total":12.5}
Be careful with the second form. A JavaScript client that reads it with JSON.parse() gets 9007199254740992, one less than the real id, so the string form is safer for public APIs.
4. Pretty-Printing with the space Argument
The third argument adds line breaks and indentation. A number gives that many spaces per level, and a string such as a tab character is used as the indent. Values above 10 count as 10, and longer strings are cut to their first 10 characters.
console.log(JSON.stringify({ x: 5, y: 6 }, null, 2));
console.log(JSON.stringify({ x: 5, y: [1, 2] }, null, "\t"));
console.log(JSON.stringify({ x: 5 }, null, 20) === JSON.stringify({ x: 5 }, null, 10));
{
"x": 5,
"y": 6
}
{
"x": 5,
"y": [
1,
2
]
}
true
Indented text is easier to read in files, logs and debugging sessions. Over the network, the extra whitespace makes every response larger and no program needs it. Send compact JSON to clients and let browser tools format it for people.
5. Custom Output with toJSON()
If an object has a method named toJSON(), JSON.stringify() calls it and converts the result instead of the object. The built-in Date class uses this hook, which is why a date comes out as an ISO 8601 string. Our own classes can use it too, for example to send money as a fixed decimal string:
class Money {
constructor(cents, currency) { this.cents = cents; this.currency = currency; }
toJSON() { return { amount: (this.cents / 100).toFixed(2), currency: this.currency }; }
}
console.log(JSON.stringify({ price: new Money(1999, "USD") }));
{"price":{"amount":"19.99","currency":"USD"}}
The hook keeps the JSON shape next to the class that owns it, so every caller gets the same output. Our guide to dates, times and money explains why APIs often send amounts as strings.
6. Everyday Uses and Their Traps
Most calls to JSON.stringify() serve one of three purposes. Each has a mistake that shows up again and again.
6.1. Sending a Request Body
The Fetch API sends the body as text. A plain object passed as body is not converted to JSON. It becomes the string “[object Object]”, which the server cannot parse. Convert the object first and label it with the Content-Type header:
const response = await fetch("https://api.example.com/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productId: 101, quantity: 2 })
});
More request patterns are on the JSON with Ajax page.
6.2. Deep Copies: Prefer structuredClone()
Many older tutorials copy objects with JSON.parse(JSON.stringify(x)). The trick works only for plain data, because every rule from section 2 applies on the way through. The built-in structuredClone() keeps dates, NaN and undefined properties intact:
const original = { when: new Date(Date.UTC(2026, 9, 2)), skip: undefined, n: NaN };
const copy = JSON.parse(JSON.stringify(original));
console.log(copy);
console.log(typeof copy.when);
const clone = structuredClone(original);
console.log(clone.when instanceof Date, Number.isNaN(clone.n), "skip" in clone);
{ when: '2026-10-02T00:00:00.000Z', n: null }
string
true true true
6.3. Placing JSON Inside an HTML Page
The method JSON.stringify() does not escape the characters that matter to HTML. A string that contains a closing script tag passes through unchanged:
console.log(JSON.stringify({ note: "</script><script>alert(1)</script>" }));
// {"note":"</script><script>alert(1)</script>"}
If a server writes that output inside a script element, the browser ends the script early and runs the injected code. Replace every < with \u003c after converting, which is still valid JSON, or pass the data through a data attribute that the template engine escapes.
7. Questions About JSON.stringify()
Two more questions show up often in searches and code reviews.
7.1. Does JSON.stringify() Keep the Order of Keys?
The method writes keys in the same order as Object.keys(). Keys that look like whole numbers come first in ascending order, and all other keys follow in the order they were added:
const o = { b: 1, a: 2 };
o[10] = 1;
o[2] = 1;
console.log(JSON.stringify(o));
// {"2":1,"10":1,"b":1,"a":2}
Since JSON objects are unordered, a client should never depend on that order anyway. Two objects with the same data can produce different strings, so comparing JSON strings is not a reliable equality check.
7.2. Why Do I See Backslashes Before Every Quote?
The value was converted twice. The second call treats the JSON text as an ordinary string and escapes its quotes, so the receiver gets text like “{\”id\”:1}”. Call JSON.stringify() once, at the point where the data leaves the program. The JSON.parse() page shows how this looks on the receiving side.
8. Conclusion
The method JSON.stringify() writes JavaScript values as JSON text, and it is strict about what survives. Watch for properties that vanish, values that turn into null, and the TypeError for BigInt and circular data. Use a replacer to control what leaves the program and toJSON() to give a class its own JSON shape. Add indentation only where people read the result. For deep copies, structuredClone() is the better tool.
9. References
These references document the behavior and the newer features used in the examples.
- MDN: JSON.stringify()
- MDN: JSON.rawJSON()
- MDN: structuredClone()
- ECMAScript specification: JSON.stringify
- RFC 8259, the JSON standard
Happy Learning !!
Comments