jsonstringifyjavascriptserializationtutorialjson-stringify

JSON.stringify() Explained: When and How to Use It

JSON Tools Team
10 min read

JSON.stringify() is one of the most frequently used methods in JavaScript, yet many developers only scratch the surface of what it can do. At its simplest, it converts a JavaScript object into a JSON string. But underneath that simple interface lies a surprisingly powerful tool with a replacer function for filtering and transforming values, a space parameter for controlling output formatting, and a set of edge-case behaviors that can trip you up if you are not aware of them.

This guide covers everything you need to know about JSON.stringify(): the basics, the advanced parameters, the gotchas, and real-world patterns you can use in production code. Whether you are sending data to an API, saving state to localStorage, or logging structured data, understanding this method thoroughly will make your code more reliable.

What Is JSON.stringify()?

JSON.stringify() is a built-in JavaScript method that serializes a JavaScript value into a JSON-formatted string. It is the counterpart to JSON.parse(), which deserializes a JSON string back into a JavaScript value. Together, these two methods form the standard way to convert between JavaScript objects and their JSON text representation.

The method signature has three parameters:

JSON.stringify(value, replacer, space)
  • value — The JavaScript value to convert. This is usually an object or array, but it can also be a string, number, boolean, or null.
  • replacer — An optional function or array that controls which properties are included and how values are transformed during serialization.
  • space — An optional number or string that adds indentation and line breaks to the output for readability. A number specifies the number of spaces; a string (like "\t") is used as the indentation character.

Basic Usage of JSON.stringify()

The most common use case is converting an object to a JSON string for transmission or storage:

const user = {
  id: 1,
  name: "Alice Chen",
  email: "alice@example.com",
  isActive: true,
  roles: ["admin", "editor"],
  address: {
    city: "San Francisco",
    state: "CA",
    zip: "94102"
  }
};

const jsonString = JSON.stringify(user);
console.log(jsonString);

Output (minified by default):

{"id":1,"name":"Alice Chen","email":"alice@example.com","isActive":true,"roles":["admin","editor"],"address":{"city":"San Francisco","state":"CA","zip":"94102"}}

Notice that the output contains no whitespace. By default, JSON.stringify() produces the most compact representation possible. This is ideal for network transmission and storage, where every byte counts. But for debugging and logging, you will want to add formatting — which is where the space parameter comes in.

Pretty-Printing with the Space Parameter

The third argument to JSON.stringify() controls indentation. Pass a number to indent by that many spaces, or pass a string to use as the indentation character:

const data = { name: "Alice", scores: [98, 87, 93], active: true };

// 2-space indentation
console.log(JSON.stringify(data, null, 2));

// 4-space indentation
console.log(JSON.stringify(data, null, 4));

// Tab indentation
console.log(JSON.stringify(data, null, "\t"));

With 2-space indentation:

{
  "name": "Alice",
  "scores": [
    98,
    87,
    93
  ],
  "active": true
}

The maximum numeric value for the space parameter is 10. If you pass a number greater than 10, it is clamped to 10. If you pass a string longer than 10 characters, only the first 10 characters are used. Passing 0, an empty string, or null produces no indentation (the same as omitting the parameter).

If you work with JSON formatting frequently, our online JSON Stringify tool lets you experiment with different formatting options interactively without writing any code.

Filtering Properties with the Replacer Parameter

The second argument — the replacer — is the most underused feature of JSON.stringify(). It gives you fine-grained control over which properties are included in the output and how their values are transformed. The replacer can be either an array of property names or a function.

Replacer as an Array

When you pass an array of strings, only those properties are included in the output. This is useful for creating a sanitized version of an object:

const userRecord = {
  id: 42,
  name: "Bob Martinez",
  email: "bob@example.com",
  passwordHash: "$2b$10$xJ3kPq...",
  ssn: "123-45-6789",
  role: "user",
  createdAt: "2025-06-15T10:30:00Z"
};

// Only include safe fields
const safeFields = ["id", "name", "email", "role", "createdAt"];
const safeJson = JSON.stringify(userRecord, safeFields, 2);
console.log(safeJson);

Output:

{
  "id": 42,
  "name": "Bob Martinez",
  "email": "bob@example.com",
  "role": "user",
  "createdAt": "2025-06-15T10:30:00Z"
}

The passwordHash and ssn fields are excluded from the output. This pattern is extremely useful for logging user data without leaking sensitive information.

Important: The array replacer only works on top-level properties of the root object. Nested properties with the same name will also be included, but you cannot selectively include nested properties by path.

Replacer as a Function

A replacer function gives you complete control. It is called for every key-value pair in the object (including nested ones), and whatever it returns becomes the value in the output. Returning undefined removes the property entirely:

const order = {
  orderId: "ORD-2025-1847",
  customer: "Acme Corp",
  items: [
    { product: "Widget A", price: 29.99, internalCost: 12.50 },
    { product: "Widget B", price: 49.99, internalCost: 22.00 }
  ],
  total: 79.98,
  internalNotes: "Priority customer, expedite shipping",
  status: "shipped"
};

const publicJson = JSON.stringify(order, (key, value) => {
  // Remove any property that starts with "internal"
  if (key.startsWith('internal')) {
    return undefined;
  }
  // Round all numbers to 2 decimal places
  if (typeof value === 'number' && key !== '') {
    return Math.round(value * 100) / 100;
  }
  return value;
}, 2);

console.log(publicJson);

Output:

{
  "orderId": "ORD-2025-1847",
  "customer": "Acme Corp",
  "items": [
    {
      "product": "Widget A",
      "price": 29.99
    },
    {
      "product": "Widget B",
      "price": 49.99
    }
  ],
  "total": 79.98,
  "status": "shipped"
}

Notice how both internalCost and internalNotes were stripped from the output, regardless of their depth in the object. The replacer function is called recursively for every property at every level.

Custom Serialization with toJSON()

If an object has a toJSON() method, JSON.stringify() calls it and uses the return value instead of the original object. This lets you define exactly how a class or object is serialized:

class DateRange {
  constructor(start, end) {
    this.start = new Date(start);
    this.end = new Date(end);
  }

  toJSON() {
    return {
      start: this.start.toISOString().split('T')[0],
      end: this.end.toISOString().split('T')[0],
      durationDays: Math.ceil(
        (this.end - this.start) / (1000 * 60 * 60 * 24)
      )
    };
  }
}

const vacation = new DateRange('2025-07-01', '2025-07-15');
console.log(JSON.stringify({ event: "Summer break", dates: vacation }, null, 2));

Output:

{
  "event": "Summer break",
  "dates": {
    "start": "2025-07-01",
    "end": "2025-07-15",
    "durationDays": 14
  }
}

The Date object itself uses toJSON() internally — that is why dates are serialized as ISO strings rather than throwing an error. You can override this behavior on your own classes to produce whatever JSON representation makes sense for your domain.

Edge Cases and Values That JSON.stringify() Drops

Not every JavaScript value can be represented in JSON. Understanding what JSON.stringify() does with non-JSON-safe values is critical for avoiding subtle bugs:

  • undefined, functions, and Symbols are silently omitted from objects. If a property's value is undefined, the entire property disappears from the output.
  • In arrays, undefined, functions, and Symbols are replaced with null (to preserve array indices).
  • NaN and Infinity are serialized as null.
  • BigInt values throw a TypeError. You must convert them to strings or numbers before serializing.
  • Circular references throw a TypeError. If object A references object B and object B references object A, JSON.stringify() cannot handle it.

Here is a demonstration:

const tricky = {
  name: "test",
  callback: function() { return true; },  // dropped
  nothing: undefined,                       // dropped
  count: NaN,                               // becomes null
  items: [1, undefined, 3],                 // undefined becomes null
  id: Symbol('unique')                      // dropped
};

console.log(JSON.stringify(tricky, null, 2));

Output:

{
  "name": "test",
  "count": null,
  "items": [
    1,
    null,
    3
  ]
}

Three properties silently vanished: callback, nothing, and id. If your code expects those properties to survive a serialize-deserialize round trip, you will have a bug. This is one of the most common sources of confusion with JSON.stringify().

Common Mistakes with JSON.stringify()

1. Assuming Round-Trip Fidelity

A common assumption is that JSON.parse(JSON.stringify(obj)) produces an exact clone of the original object. It does not. As we saw above, undefined values, functions, Symbols, Date objects (which become strings), Map, Set, RegExp, and many other types are lost or transformed during serialization. If you need a true deep clone, use structuredClone() (available in modern browsers and Node.js 17+).

2. Forgetting the Replacer When Logging Sensitive Data

If you log user objects to the console or a file using JSON.stringify(user), you might accidentally include passwords, tokens, or personal data. Always use a replacer to strip sensitive fields from log output.

3. Double-Stringifying

Calling JSON.stringify() on a value that is already a string wraps it in quotes and escapes internal quotes. The result is a string that looks like "{\"name\":\"Alice\"}" — valid JSON, but representing a string value rather than an object. If you then send this to an API, the server receives a string instead of an object. Always check whether your value is already a string before stringifying it.

4. Ignoring Circular References

If you try to stringify an object with circular references, you get a TypeError: Converting circular structure to JSON. This commonly happens with DOM elements, Mongoose documents, or any object graph with bidirectional relationships. Use a replacer function to break circular references, or use a library like flatted or json-stringify-safe.

Best Practices for Using JSON.stringify()

  • Use the space parameter only for debugging and display. In production code that sends data over the network, omit the space parameter to produce the smallest possible payload.
  • Use a replacer to sanitize sensitive data. Create a reusable replacer function that strips passwords, tokens, and PII from objects before logging or transmitting them.
  • Handle edge cases explicitly. If your objects might contain undefined, NaN, or BigInt values, use a replacer to convert them to JSON-safe representations rather than letting them silently disappear.
  • Define toJSON() on custom classes. If you have domain objects with complex internal state, implement toJSON() to control their serialization explicitly.
  • Catch errors from circular references. Wrap JSON.stringify() in a try-catch when working with objects that might contain cycles, such as ORM models or deeply interconnected data structures.
  • Validate your output. After stringifying complex objects, verify the result is valid JSON using a JSON Validator. This catches issues with custom toJSON() implementations or unexpected data types.

Real-World Patterns

Storing State in localStorage

localStorage only stores strings, so JSON.stringify() is the standard way to persist structured data in the browser:

// Save
const appState = {
  theme: "dark",
  sidebar: { collapsed: false, width: 280 },
  recentFiles: ["report.json", "config.json"]
};
localStorage.setItem('appState', JSON.stringify(appState));

// Load
const saved = localStorage.getItem('appState');
const state = saved ? JSON.parse(saved) : defaultState;

Creating Deterministic Hashes

If you need to hash a JSON object (for caching, deduplication, or ETags), key order matters. Two objects with the same properties in different order produce different strings and therefore different hashes. Use a replacer or sorted-keys approach for deterministic output:

function stableStringify(obj) {
  return JSON.stringify(obj, (key, value) => {
    if (value && typeof value === 'object' && !Array.isArray(value)) {
      return Object.keys(value).sort().reduce((sorted, k) => {
        sorted[k] = value[k];
        return sorted;
      }, {});
    }
    return value;
  });
}

const a = { name: "Alice", age: 30 };
const b = { age: 30, name: "Alice" };

console.log(stableStringify(a) === stableStringify(b)); // true

Try It Now

Want to experiment with JSON.stringify() without setting up a coding environment? Our online JSON Stringify tool lets you paste any JavaScript object and see the stringified output instantly. You can toggle formatting, test different indentation levels, and copy the result with one click.

Need to go the other direction? Use the JSON Unstringify tool to convert an escaped JSON string back into a readable object. And if you want to pretty-print the result, our JSON Formatter gives you clean, indented output in any style you prefer.

Conclusion

JSON.stringify() does far more than convert objects to strings. With the replacer parameter, you can filter sensitive data, transform values, and control exactly what gets serialized. With the space parameter, you can produce human-readable output for debugging and documentation. And with toJSON(), you can define custom serialization logic for your own classes.

The most important thing to remember is what JSON.stringify() cannot do: it cannot represent undefined, functions, Symbols, BigInt, or circular references. If your objects contain any of these, you need to handle them explicitly with a replacer or a preprocessing step. Ignoring these edge cases leads to data silently disappearing — the kind of bug that is hard to notice and harder to debug.

Master the three parameters, understand the edge cases, and JSON.stringify() becomes one of the most versatile tools in your JavaScript toolkit.