← All articles

How to Stringify JSON with JSON.stringify: Values, Replacer, Space, and Escaping

Learn how JSON.stringify works in real JavaScript code: request bodies, pretty-printing, replacer functions, toJSON, BigInt, Map, Set, circular references, and JSON string literals.

Quick answer: use JSON.stringify(value) to turn a JavaScript value into JSON text. Add a third argument such as 2 to pretty-print: JSON.stringify(value, null, 2). Use the second argument, replacer, when you need to redact fields, convert unsupported values, or include only certain keys. Do not build JSON with string concatenation.

The simple version looks like this:

const payload = {
  userId: 'u_123',
  active: true,
  roles: ['admin', 'editor']
};

const body = JSON.stringify(payload);
// {"userId":"u_123","active":true,"roles":["admin","editor"]}

That string is ready for a JSON request body, a file, a message queue payload, or storage. But JSON.stringify is not a magic "make anything safe" button. It has rules, and those rules explain many bugs: missing properties, null values that used to be NaN, broken BigInt serialization, {} from Map, and circular-reference errors.

What JSON.stringify Does

JSON.stringify(value, replacer, space) walks a JavaScript value and returns a JSON string. JSON itself can only represent:

  • string
  • number
  • boolean
  • null
  • object
  • array

Everything else must be converted, omitted, or rejected.

Examples:

JSON.stringify('hello');
// "hello"

JSON.stringify(42);
// 42

JSON.stringify(true);
// true

JSON.stringify(null);
// null

JSON.stringify({ name: 'Ada', score: 98 });
// {"name":"Ada","score":98}

Top-level values are allowed. A JSON document can be an object, array, string, number, boolean, or null.

Use It for Request Bodies

The most common production use is fetch:

const response = await fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Ada',
    email: 'ada@example.com'
  })
});

Two mistakes show up constantly:

// Wrong: the object may become "[object Object]"
body: payload

// Wrong: hand-built JSON breaks on quotes, newlines, and backslashes
body: `{"name":"${name}"}`

JSON.stringify escapes the dangerous characters for you. If name contains a quote, newline, tab, or backslash, the output is still valid JSON.

Do You Need JSON Text or a JSON String Literal?

People use "stringify JSON" for two related jobs:

Job Input Output Example use
Serialize a value JavaScript object, array, number, etc. JSON text API body, file, localStorage
Escape text as a JSON string literal Plain text Quoted and escaped JSON string Nested JSON, logs, database field, shell argument

Value serialization:

JSON.stringify({ name: 'Ada' });
// {"name":"Ada"}

String-literal escaping:

JSON.stringify('She said "hello"\nThen left.');
// "She said \"hello\"\nThen left."

The JSON Stringify tool on this site is built for the second workflow. It takes plain text from input.txt, runs JSON.stringify(text), and returns an escaped JSON string literal. Click Unstringify to parse a JSON string literal back into plain text.

That distinction matters when you are debugging double-encoded JSON. This is JSON text:

{"name":"Ada"}

This is a JSON string that contains JSON-looking text:

"{\"name\":\"Ada\"}"

If you receive the second form, parse once to get the inner string, then parse again if you need the object.

The Three Arguments

JSON.stringify has this shape:

JSON.stringify(value, replacer, space)
Argument Required? What it does
value Yes The JavaScript value to serialize
replacer No Filters or transforms values
space No Adds indentation for readable output

The default output is compact. That is good for request bodies and storage. Use space when people need to read the result.

Pretty-Print with the space Argument

Pass null for the replacer and 2 for two-space indentation:

const user = { name: 'Ada', age: 36, active: true };

JSON.stringify(user, null, 2);

Output:

{
  "name": "Ada",
  "age": 36,
  "active": true
}

Use 4 for four spaces or '\t' for tabs:

JSON.stringify(user, null, 4);
JSON.stringify(user, null, '\t');

JavaScript caps numeric indentation at 10 spaces. String indentation is also truncated to 10 characters. Passing 100 will not create a 100-space indent.

For the full formatting workflow, see How to Format JSON. For compact output and byte-size tradeoffs, see How to Minify JSON.

Filter Keys with a Replacer Array

The second argument can be an array of property names to include:

const user = {
  id: 'u_123',
  name: 'Ada',
  email: 'ada@example.com',
  passwordHash: 'do-not-send',
  active: true
};

JSON.stringify(user, ['id', 'name', 'active']);
// {"id":"u_123","name":"Ada","active":true}

This is an allow-list, not a block-list. If a key is missing from the array, it will not appear in the output.

Be careful with nested data. A replacer array applies by property name at every level:

const order = {
  id: 'ord_1',
  customer: {
    id: 'cus_1',
    email: 'ada@example.com'
  }
};

JSON.stringify(order, ['id', 'customer']);
// {"id":"ord_1","customer":{"id":"cus_1"}}

The nested email field is omitted because email was not in the allow-list.

Transform Values with a Replacer Function

A replacer function runs for the root value and then for each property. It receives (key, value) and returns the value that should be serialized.

Redact secrets:

const payload = {
  user: 'ada',
  password: 'secret',
  token: 'abc123'
};

const safe = JSON.stringify(payload, (key, value) => {
  if (key === 'password') return '[REDACTED]';
  if (key === 'token') return undefined;
  return value;
});

// {"user":"ada","password":"[REDACTED]"}

Returning undefined from a replacer omits an object property. In an array, it becomes null because JSON arrays cannot have missing slots.

Convert values JSON cannot represent:

const output = JSON.stringify(
  { id: 9007199254740993n, createdAt: new Date('2026-05-24T00:00:00Z') },
  (key, value) => {
    if (typeof value === 'bigint') return value.toString();
    return value;
  }
);

// {"id":"9007199254740993","createdAt":"2026-05-24T00:00:00.000Z"}

One subtle detail: the replacer is first called for the whole root value with key === ''. If you return undefined there, the entire stringify result is undefined.

The toJSON Hook Runs Before Serialization

If an object has a toJSON() method, JSON.stringify calls it and serializes the return value.

That is why Date becomes an ISO string:

JSON.stringify({ when: new Date('2026-05-24T00:00:00Z') });
// {"when":"2026-05-24T00:00:00.000Z"}

You can define your own:

class Money {
  constructor(cents, currency = 'USD') {
    this.cents = cents;
    this.currency = currency;
  }

  toJSON() {
    return {
      amount: (this.cents / 100).toFixed(2),
      currency: this.currency
    };
  }
}

JSON.stringify({ price: new Money(1999) });
// {"price":{"amount":"19.99","currency":"USD"}}

Use toJSON when a class has one obvious JSON representation. Use a replacer when the same value needs different output in different contexts, such as public API responses versus internal logs.

What Gets Dropped, Converted, or Rejected

Here is the table worth keeping near your debugger:

Value In an object In an array At the root
undefined Property omitted null undefined
Function Property omitted null undefined
Symbol value Property omitted null undefined
Symbol-keyed property Ignored N/A N/A
NaN null null null
Infinity / -Infinity null null null
BigInt Throws Throws Throws
Date ISO string ISO string ISO string
Map {} unless converted {} unless converted {} unless converted
Set {} unless converted {} unless converted {} unless converted

This is why data "disappears" after stringifying. JSON has no representation for many JavaScript values, so you need to decide how they should appear before serialization.

Map, Set, and Custom Collections

Map and Set do not serialize the way most developers expect:

const permissions = new Map([
  ['read', true],
  ['write', false]
]);

JSON.stringify(permissions);
// {}

Convert them first:

JSON.stringify([...permissions]);
// [["read",true],["write",false]]

JSON.stringify(Object.fromEntries(permissions));
// {"read":true,"write":false}

For Set:

const roles = new Set(['admin', 'editor']);

JSON.stringify([...roles]);
// ["admin","editor"]

If you need to reconstruct the types later, pair the stringify step with a JSON.parse reviver:

const json = JSON.stringify({ roles: [...roles] });

const parsed = JSON.parse(json, (key, value) => {
  if (key === 'roles') return new Set(value);
  return value;
});

BigInt and Large Number IDs

BigInt throws because JSON has no BigInt type:

JSON.stringify({ id: 9007199254740993n });
// TypeError: Do not know how to serialize a BigInt

Convert it deliberately:

JSON.stringify({ id: 9007199254740993n }, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
);
// {"id":"9007199254740993"}

Large plain numbers are a different issue. JavaScript numbers are IEEE 754 doubles. Integers larger than Number.MAX_SAFE_INTEGER may already be imprecise before you stringify them:

Number.MAX_SAFE_INTEGER;
// 9007199254740991

If an ID must be exact, store it as a string in JSON:

{ "account_id": "9223372036854775807" }

Do not "fix" this after parsing. Once precision is lost, the original number is gone.

Circular References Throw

JSON is a tree. A JavaScript object graph can have cycles. JSON.stringify rejects cycles:

const user = { name: 'Ada' };
user.self = user;

JSON.stringify(user);
// TypeError: Converting circular structure to JSON

For logging, you can omit repeated objects:

function omitCircularReferences() {
  const seen = new WeakSet();

  return (key, value) => {
    if (value && typeof value === 'object') {
      if (seen.has(value)) return '[Circular]';
      seen.add(value);
    }
    return value;
  };
}

JSON.stringify(user, omitCircularReferences(), 2);

For real API payloads, prefer changing the data shape. A circular object usually means the transport model should contain IDs or nested summaries, not the full object graph.

Do Not Use JSON.stringify for Deep Cloning

This old trick loses data:

const copy = JSON.parse(JSON.stringify(value));

It drops undefined, functions, Symbols, Maps, Sets, BigInts, circular references, and prototype information. Dates come back as strings.

Use structuredClone for in-memory cloning when your runtime supports it:

const copy = structuredClone(value);

Use JSON round-trips only when you intentionally want a JSON-compatible projection of the data.

Stable and Canonical Stringify

JSON.stringify generally follows JavaScript property enumeration order. That is stable for normal objects in one runtime, but it is not a canonicalization system for signatures or hashes.

If the JSON text itself will be hashed, signed, cached by string value, or compared in snapshots, choose a deterministic strategy:

  • Recursively sort object keys before stringifying.
  • Use a stable stringify library.
  • Use a formal canonicalization scheme such as JSON Canonicalization Scheme when signatures depend on exact bytes.

For ordinary API bodies, you rarely need this. JSON consumers should parse the value rather than compare raw text.

Stringify in Other Languages

Python:

import json

json.dumps({"name": "Ada"})
json.dumps({"name": "Ada"}, indent=2)
json.dumps({"name": "Ada"}, separators=(",", ":"))

Python's default= option is closest to a JavaScript replacer for unsupported custom objects:

import json
from decimal import Decimal

def encode(value):
    if isinstance(value, Decimal):
        return str(value)
    raise TypeError(f"Cannot encode {type(value).__name__}")

json.dumps({"price": Decimal("19.99")}, default=encode)

Go:

package main

import "encoding/json"

type User struct {
  Name string `json:"name"`
}

func main() {
  b, _ := json.Marshal(User{Name: "Ada"})
  _ = b
}

Ruby:

require "json"

JSON.generate({ name: "Ada" })
JSON.pretty_generate({ name: "Ada" })

Escape and Unescape a JSON String Literal

Use JSON Stringify when the thing you need is not an API body, but an escaped string literal.

Example input text:

{"message": "Hello\nworld"}

Stringified output:

"{\"message\": \"Hello\\nworld\"}"

Use cases:

  • Put a JSON-looking value inside another JSON document.
  • Store raw JSON text in a database text column.
  • Paste a string safely into a shell command or test fixture.
  • Decode a double-encoded log value with Unstringify.

The tool runs the core escape/unescape workflow in your browser. It uses the same primitive as the code above: JSON.stringify(text) for escaping and JSON parsing for unescaping.

Practical Checklist

Before calling JSON.stringify in production code:

  1. Decide whether you need compact JSON, pretty JSON, or a JSON string literal.
  2. Never concatenate user input into JSON text.
  3. Add Content-Type: application/json when sending request bodies.
  4. Convert BigInt, Map, Set, Date, and custom classes deliberately.
  5. Redact secrets with an allow-list or replacer before logging.
  6. Treat circular references as a data-shape problem, not only a serialization problem.
  7. Keep large IDs as strings if exact precision matters.
  8. Use stable stringify only when raw text equality matters.

Frequently Asked Questions

How do I pretty-print with JSON.stringify?

Pass a third argument: JSON.stringify(value, null, 2) for two-space indentation, 4 for four spaces, or '\t' for tabs. Numeric indentation is capped at 10 spaces.

Why does JSON.stringify drop some of my properties?

JSON has no representation for undefined, functions, or Symbols. In objects those properties are omitted. In arrays they become null. Use a replacer or convert the values before stringifying.

How do I stringify a value that contains BigInt?

JSON.stringify() throws on BigInt. Convert bigint values to strings with a replacer, then parse them back deliberately on the receiving side if you need bigint behavior again.

What is the difference between JSON.stringify and JSON.parse?

JSON.stringify turns a JavaScript value into JSON text. JSON.parse turns JSON text back into a JavaScript value. They are paired operations, but they are not lossless for values JSON cannot represent.

Why does JSON.stringify(new Map()) return {}?

Map and Set do not expose their entries as enumerable object properties, so JSON.stringify sees an empty object. Convert a Map with [...map] or Object.fromEntries(map), and convert a Set with [...set].

How do I avoid circular reference errors?

For logs, use a replacer with a WeakSet to mark repeated objects. For APIs, redesign the payload so it contains IDs or summaries instead of a circular object graph.

Is JSON.stringify safe for logging?

It is safe for escaping syntax, but not automatically safe for privacy. It can include tokens, passwords, cookies, or private user fields unless you redact them first with an allow-list or replacer.

How do I stringify JSON in Python?

Use json.dumps(value) for compact JSON, json.dumps(value, indent=2) for pretty output, and json.dump(value, file) to write directly to a file. Use default= to encode custom objects such as Decimal.

How do I unstringify a JSON string literal?

Use JSON.parse on the string literal. If the result is another JSON-looking string, parse once to decode the string literal and parse a second time to get the inner object.

Related JSON Tools and Guides

Sources

Last reviewed July 2026.