JSON.stringify: Every Behavior With Practical Examples
Quick answer
💡JSON.stringify(value, replacer, space) converts a JavaScript value to a JSON string. It silently drops keys whose values are undefined, Function, or Symbol. Pass a replacer array to whitelist keys, a replacer function to control serialization, and a number or string as the space parameter to produce indented output. BigInt and circular references throw TypeError.
Error symptoms
- ✕
Object properties disappear from the JSON output without any warning or error being thrown - ✕
TypeError: Do not know how to serialize a BigInt when an object contains a BigInt value - ✕
TypeError: Converting circular structure to JSON when an object references itself or an ancestor - ✕
Date objects serialize as ISO strings but comparing them after JSON.parse returns a string, not a Date instance - ✕
JSON.stringify returns undefined instead of a string when called with undefined or a function as the value - ✕
Large objects take noticeable time to serialize because JSON.stringify blocks the event loop during serialization
Common causes
- •Setting object properties to undefined, Function, or Symbol, which are all silently omitted by JSON.stringify
- •Including a BigInt value in an object without defining a toJSON method on BigInt.prototype
- •Creating circular data structures where an object's parent or child property points back to an ancestor in the chain
- •Expecting Date objects to survive a JSON round-trip as Date instances when they serialize to strings and parse back as strings
- •Using JSON.stringify on large objects or deeply nested structures with thousands of nodes in a request handler
- •Passing a replacer array but omitting intermediate keys that are needed to reach a nested property
When it happens
- •When serializing Redux or Zustand state to localStorage and the state contains non-serializable values like function callbacks
- •When sending API request bodies with JSON.stringify and some fields are conditionally undefined based on user input
- •When working with financial calculations that use BigInt for precision and trying to include the result in a JSON payload
- •When building a graph data structure with parent and child references and attempting to serialize the entire graph
- •When serializing large database query results in a serverless function where blocking the event loop is expensive
Examples and fixes
A replacer function is called for every key-value pair during serialization. Returning undefined from the replacer omits that key from the output. Returning a different value transforms the output for that key.
Using a replacer function to filter and transform properties
❌ Wrong
const transactionRecord = {
transactionId: 'txn_44821',
amount: 149.99,
currency: 'USD',
cardLastFour: '4242',
cardholderName: 'Elena Marchetti',
internalAuditId: 'audit_0039',
processedAt: new Date('2026-05-05T14:30:00Z'),
callbackFn: () => console.log('processed')
};
// No replacer: sensitive fields included, function dropped silently
const apiResponse = JSON.stringify(transactionRecord);
console.log(apiResponse);
// callbackFn is gone silently; sensitive fields like cardLastFour are exposed✅ Fixed
const transactionRecord = {
transactionId: 'txn_44821',
amount: 149.99,
currency: 'USD',
cardLastFour: '4242',
cardholderName: 'Elena Marchetti',
internalAuditId: 'audit_0039',
processedAt: new Date('2026-05-05T14:30:00Z'),
callbackFn: () => console.log('processed')
};
// Replacer function: omit sensitive and internal fields
function apiReplacer(key, value) {
const omittedKeys = ['cardLastFour', 'cardholderName', 'internalAuditId', 'callbackFn'];
if (omittedKeys.includes(key)) return undefined;
if (value instanceof Date) return value.toISOString();
return value;
}
const apiResponse = JSON.stringify(transactionRecord, apiReplacer, 2);
console.log(apiResponse);
// Only transactionId, amount, currency, and processedAt appear in outputThe broken version passes no replacer, causing JSON.stringify to silently drop the callbackFn property without any indication that data was omitted. More critically, it includes sensitive fields like cardLastFour and cardholderName in the output that should not appear in an API response. The fixed version uses a replacer function that returns undefined for fields that should be omitted and handles Date serialization explicitly. The replacer receives the key as an empty string for the root value, then as the property name for each nested key. The additional space parameter of 2 formats the output with 2-space indentation for readability.
When JSON.stringify encounters an object that has a toJSON method, it calls that method and uses the return value as the serialized representation. This allows class instances to define their own JSON representation.
Defining toJSON on a class to control serialization
❌ Wrong
class MonetaryAmount {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
this._internalPrecision = 8;
this._auditLog = [];
}
}
const price = new MonetaryAmount(29.99, 'EUR');
const orderPayload = {
orderId: 'ord_8821',
totalPrice: price,
itemCount: 3
};
console.log(JSON.stringify(orderPayload, null, 2));
// Includes _internalPrecision and _auditLog which are implementation details✅ Fixed
class MonetaryAmount {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
this._internalPrecision = 8;
this._auditLog = [];
}
toJSON() {
return {
amount: parseFloat(this.amount.toFixed(2)),
currency: this.currency,
formatted: `${this.currency} ${this.amount.toFixed(2)}`
};
}
}
const price = new MonetaryAmount(29.99, 'EUR');
const orderPayload = {
orderId: 'ord_8821',
totalPrice: price,
itemCount: 3
};
console.log(JSON.stringify(orderPayload, null, 2));
// totalPrice serializes as {amount: 29.99, currency: 'EUR', formatted: 'EUR 29.99'}
// _internalPrecision and _auditLog are excludedThe broken version serializes the MonetaryAmount instance by enumerating all its own properties, including internal implementation details like _internalPrecision and _auditLog that have no meaning for API consumers. The fixed version adds a toJSON method that returns a plain object with only the properties that are meaningful for serialization. JSON.stringify checks for toJSON before serializing any object, and if it exists and is a function, it calls toJSON() and uses the returned value. This pattern works throughout nested structures: the orderPayload object is serialized normally, but when JSON.stringify reaches the totalPrice property, it calls price.toJSON() automatically.
What stringify silently drops from JavaScript objects
JSON.stringify enforces the boundary between JavaScript's type system and JSON's more constrained type system by silently omitting values that have no JSON representation. Understanding exactly which values are dropped, and in which context, is essential for avoiding data loss during serialization.
The three value types that JSON.stringify silently omits from object properties are undefined, Function, and Symbol. When a property's value is any of these three types, JSON.stringify removes the entire key-value pair from the output without throwing an error and without any other indication. The returned JSON string will have fewer keys than the original object, and no warning is emitted. This behavior is consistent with the specification and intentional: these types have no meaningful JSON representation, and silently omitting them allows the remaining properties to serialize correctly.
The behavior differs for arrays. When an array element is undefined, a Function, or a Symbol, JSON.stringify does not remove the element. Instead, it converts it to the JSON literal null. This preserves the length of the array and the positional meaning of elements after the undefined position. An array like [1, undefined, 3] becomes the JSON string [1,null,3], not [1,3], which would change the indices of remaining elements.
Date objects have a special serialization path: when JSON.stringify encounters a Date, it calls the Date's toJSON() method, which returns the ISO 8601 string representation of the date, equivalent to calling toISOString(). The Date serializes to a string like 2026-05-05T14:30:00.000Z. The critical point is that JSON.parse does not reconstruct a Date object from this string; it returns the string value as-is. A round-trip through JSON.stringify and JSON.parse converts a Date to a string, and any code that expects a Date object after deserialization must reconstruct it explicitly.
Regular expressions, Map, Set, and most other built-in JavaScript objects do not have a toJSON method and cannot be meaningfully serialized to JSON. A RegExp serializes as an empty object {} because its internal state is not enumerable. A Map serializes as an empty object {} because Map entries are not own enumerable properties. A Set serializes similarly. If any of these types appear in data that must be serialized, either define a toJSON method on the object or convert the value to a serializable representation before calling JSON.stringify.
Filtering object keys with replacer arrays and functions
The replacer parameter of JSON.stringify provides two mechanisms for controlling which keys appear in the output. A replacer array serves as a whitelist: only keys whose names appear in the array will be included in the serialized output. A replacer function provides full programmatic control: it is called for every key-value pair encountered during serialization, and its return value determines what appears in the JSON output for that entry.
When using a replacer array, the array must contain the string names of every key you want to include, at every level of nesting. This is a subtle requirement: if you want to include a nested property like address.city, the string address must appear in the replacer array (to include the address object), and the string city must also appear (to include the city property inside the address object). If you include city but not address, the address object is excluded entirely, taking city with it. The replacer array filters by key name only, not by path, so a key name in the array is included wherever it appears in the object tree.
A replacer function gives complete control over serialization. The function receives two arguments: the current key and the current value. For the root value being serialized, the function is called with an empty string as the key and the root value as the value. For every subsequent key-value pair in the traversal, the function is called with the property name as the key. The return value of the replacer function is used as the serialized value for that entry. Returning undefined removes the key from the output. Returning a different value replaces the original value with the returned value in the JSON output.
The replacer function is called in the same traversal order as JSON.stringify's normal serialization, which is the order of own enumerable properties as returned by Object.keys. For arrays, the indices are passed as string representations of the numbers. The function approach is more powerful than the array approach for dynamic scenarios like omitting keys based on their value, applying transformations to specific types, or logging which keys are being serialized for debugging purposes.
Defining toJSON on class instances for clean serialization
The toJSON() method is a convention that JSON.stringify recognizes and uses during serialization. When the serializer encounters an object, it checks whether the object has a toJSON property and whether it is a function. If so, it calls toJSON() with the current key as the argument, and uses the returned value in place of the original object for the rest of serialization. If toJSON() returns another object, that object is serialized normally. If toJSON() returns a primitive, that primitive is the serialized value.
Defining toJSON on a class is the cleanest way to control how class instances appear in JSON output. The toJSON method has full access to the instance via this, so it can read private or computed properties and return a plain object containing exactly the fields that should appear in JSON. This approach encapsulates the serialization logic within the class rather than requiring every call site to use a replacer function.
Date.prototype.toJSON is the most widely used built-in toJSON implementation. It returns the ISO 8601 string representation of the date, which is how JSON.stringify converts Date objects to strings automatically. You can override Date.prototype.toJSON if you need dates to serialize in a different format, though this is generally a bad practice because it changes the global behavior for all Date objects in the process. A better approach is to create a wrapper class with its own toJSON that formats the date in the required way.
For third-party class instances that you cannot modify, a replacer function is the correct mechanism. The replacer function can check whether the value is an instance of the specific class using instanceof, and return a plain object representation if it is. This approach keeps the serialization logic outside the class definition and is appropriate for adapting library types to a specific JSON format.
One important nuance: if an object has both a toJSON method and entries in a replacer array or function, both mechanisms apply. The toJSON method is called first to get the serialized representation of the object, and then the replacer is applied to the result. If toJSON returns an object, the replacer can further filter or transform that object's properties.
Space parameter for producing readable JSON output
The third parameter to JSON.stringify controls the indentation of the output and has a significant effect on readability. When the space parameter is omitted or is zero, JSON.stringify produces compact output with no whitespace between elements. Every key-value pair, comma, and brace appears immediately adjacent to its neighbors, producing the most compact representation but also the least human-readable.
When space is a positive integer, JSON.stringify inserts that many space characters before each nested value. A space value of 2 produces the most commonly used human-readable format, with each level of nesting indented by two additional spaces. A space value of 4 is also common. Values above 10 are clamped to 10 by the specification, though most practical use cases do not need more than 4. The indented output also includes newline characters after each value, making it suitable for writing to files or displaying in a code editor.
When space is a string, that string is used as the indentation unit instead of spaces. Common values are '\t' for tab indentation and ' ' (single space) for minimal indentation. If the string is longer than 10 characters, only the first 10 characters are used. Using a tab character as the indentation string produces output that respects editor tab-width settings, which some style guides prefer.
The space parameter affects only the JSON structure's whitespace, not the values themselves. String values that contain spaces, newlines, or other whitespace are always serialized with the content preserved exactly as JSON string escape sequences. The indentation whitespace appears only between structural characters like commas, colons, braces, and brackets, never inside string values.
For API responses, compact output without the space parameter is standard because it minimizes response body size. For configuration files, log files, or any JSON that humans will read directly, the 2-space indented format is the conventional choice. Some development tools like Node.js's util.inspect accept similar formatting options, and knowing the JSON.stringify space parameter allows quick production of readable JSON for debugging output at development time.
When BigInt values and circular references cause TypeError
BigInt is a JavaScript primitive type introduced in ES2020 for representing integers of arbitrary size. Unlike number, which uses IEEE 754 double-precision floating-point, BigInt can represent integers larger than 2 raised to the 53rd power without losing precision. This makes BigInt useful for financial calculations, database primary keys, and cryptographic operations where exact integer arithmetic is required.
JSON.stringify throws TypeError: Do not know how to serialize a BigInt whenever it encounters a BigInt value during serialization. The error message is specific and clear, but the timing can be surprising: if the BigInt is deeply nested inside a large object, the error is thrown partway through serialization, and the partial output is discarded. The fix is to convert BigInt values to strings or numbers before serialization. For most use cases, converting to a string with bigIntValue.toString() is appropriate because it preserves the full precision. Converting to a number with Number(bigIntValue) is only safe for BigInt values that are within the safe integer range (Number.MAX_SAFE_INTEGER).
Circular references occur when an object's property tree contains a reference back to an ancestor object. A simple case is const obj = {}; obj.self = obj;. Calling JSON.stringify(obj) throws TypeError: Converting circular structure to JSON because the serializer follows the reference to obj.self, finds obj again, follows obj.self again, and continues forever until the stack overflows. The error message in V8 is more detailed and shows the path that leads to the cycle: it reports that while serializing property 'self', the serializer arrived back at the starting object.
For objects that may contain circular references, a replacer function can detect cycles by maintaining a Set of objects that have been seen so far in the traversal. If the current value is an object that is already in the Set, the replacer returns a sentinel like '[Circular]' instead of the object. If it is a new object, it is added to the Set and returned normally. This approach serializes the first occurrence of each object and replaces subsequent occurrences with a marker.
Libraries like flatted and circular-json provide drop-in replacements for JSON.stringify and JSON.parse that handle circular references by encoding the structure as a flat array with index references. These libraries allow round-tripping circular structures through JSON, though the output is not standard JSON and can only be parsed by the same library.
Streaming alternatives for large JSON payloads
JSON.stringify is synchronous and blocking. When called on a large object, it occupies the JavaScript event loop for the entire duration of serialization, preventing other tasks from being processed. For small to medium objects, this is not noticeable. For objects with tens of thousands of nested nodes, or for objects containing large arrays, the blocking time can be significant enough to affect the response time of a web server or the responsiveness of a browser application.
The fast-json-stringify library is a popular alternative for high-throughput Node.js applications. It takes a JSON Schema as input and generates a serialization function that is optimized for the specific shape of data described by the schema. Because the generated function knows the types of all properties in advance, it can skip the type-checking overhead that JSON.stringify performs for every value. Benchmarks show that fast-json-stringify can serialize compatible objects several times faster than JSON.stringify for schemas with many properties.
For HTTP streaming, the content-type: application/x-ndjson format (Newline-Delimited JSON) allows sending multiple JSON objects as separate lines in a stream. Each line is a complete, self-contained JSON document, and the client reads lines one at a time as they arrive rather than waiting for the entire response. This pattern is common for server-sent events, live log streaming, and large dataset exports where the client should start processing results before all results are available. Implementing NDJSON in Node.js is straightforward with readable streams and the readline module.
For truly massive objects that cannot fit in memory as a complete JSON string, streaming JSON serializers like JSONStream and stream-json allow serialization of arrays and objects in chunks. These libraries produce JSON output as a stream of small buffer chunks rather than a single large string, which keeps memory usage proportional to the size of each chunk rather than the total object size. The output can be piped directly to an HTTP response stream, a file write stream, or a compression stream without buffering the entire JSON string in memory.
When performance matters, measuring is more valuable than guessing. Profiling JSON.stringify with Node.js's built-in performance hooks or with clinic.js can identify whether JSON serialization is actually the bottleneck before investing in optimization. In many production systems, database queries, network round-trips, or business logic processing dominate response times, and JSON serialization is not a significant contributor. Optimize only after profiling confirms that serialization time is a measurable factor.
Quick fix checklist
- ✓Check for undefined, Function, and Symbol values in objects before calling JSON.stringify to prevent silent key drops
- ✓Use a replacer function or replacer array to explicitly control which keys appear in the JSON output
- ✓Define toJSON() on class instances to prevent internal fields from leaking into API responses
- ✓Convert BigInt values to strings with .toString() before serializing to avoid TypeError
- ✓Detect circular references with a WeakSet in a replacer function or use a library like flatted
- ✓Remember that Date objects serialize to ISO strings and must be reconstructed explicitly after JSON.parse
- ✓Use space parameter 2 for human-readable debug output and omit it for production API responses
- ✓Consider fast-json-stringify or streaming alternatives for large payloads that block the event loop
Related guides
Frequently asked questions
Why does JSON.stringify silently drop some object properties?
JSON.stringify omits properties whose values are undefined, Function, or Symbol because these types have no representation in the JSON specification. Rather than throwing an error, the specification defines that these values should be omitted when they appear as object property values. For array elements, undefined and Function are converted to null to preserve the array's length and positional indices. This behavior is intentional but surprising to developers expecting a complete copy of the object.
How do I serialize an object but include only specific fields?
Pass an array of field names as the second argument to JSON.stringify. For example, JSON.stringify(userData, ['name', 'email', 'role']) produces a JSON string containing only the name, email, and role properties from userData, omitting all other fields. For nested objects, include the property names of the nested fields as well, because the replacer array filters by property name at all levels of nesting.
Can I make JSON.stringify handle BigInt without throwing?
Yes, define a toJSON method on BigInt.prototype: BigInt.prototype.toJSON = function() { return this.toString(); }. This converts all BigInt values to strings during serialization automatically. Alternatively, use a replacer function that checks typeof value === 'bigint' and returns value.toString(). The string representation preserves full precision, unlike converting to a Number which loses precision for values larger than Number.MAX_SAFE_INTEGER.
What happens when JSON.stringify encounters a Date?
JSON.stringify calls the Date's toJSON() method, which returns the ISO 8601 string like 2026-05-05T14:30:00.000Z. This string is then included in the JSON output as a regular string value. When JSON.parse reads this string back, it returns the ISO string as a plain string, not a Date object. To get a Date back after a round-trip, you must explicitly call new Date(parsedString.dateField) on each date field in the parsed result.
How do I pretty-print JSON with JSON.stringify?
Pass a number as the third argument for the number of spaces per indent level. JSON.stringify(data, null, 2) produces output indented with 2 spaces per level, which is the most common human-readable format. Pass null as the replacer when you want no filtering but still need the space parameter. You can also pass a string like '\t' for tab indentation or a custom string for custom indent characters.
How do I handle circular references in JSON.stringify?
Use a replacer function that tracks visited objects with a WeakSet. Before serializing each value, check if it is an object that has already been seen. If so, return a string like '[Circular Reference]' instead of the object. If not, add it to the WeakSet and return it normally. Alternatively, use the flatted library which provides JSON.stringify and JSON.parse replacements that encode circular structures as flat arrays with index references.
Does JSON.stringify modify the original object in any way?
No, JSON.stringify does not modify the original object. It reads the object's properties during traversal but does not write to it. If an object has a toJSON method, that method is called and its return value is serialized, but the toJSON method operates on the object and can return whatever it wants without altering the object itself. The original object remains unchanged after JSON.stringify completes.
What is the performance impact of JSON.stringify on large objects?
JSON.stringify is synchronous and blocks the Node.js event loop for the full duration of serialization. For objects with thousands of properties or deeply nested structures, this can take tens of milliseconds, which is significant for high-throughput servers. For large datasets, consider fast-json-stringify with a pre-compiled schema, which can be several times faster. For streaming large arrays, use JSONStream or stream-json to serialize in chunks without buffering the full output.
What does the replacer function receive for the root value?
The replacer function is called once for the root value with an empty string as the key argument and the root value as the value argument. This is a quirk of the specification that can cause confusion: checking if key === '' in the replacer identifies the root call. All subsequent calls for nested properties use the actual property name or array index as the key. Most replacer functions ignore the root call by returning the value unchanged when key is empty.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-05.