JSON escape characters

Seven characters plus everything below U+0020 must be escaped. The forward slash and non-ASCII text must not — escaping them is optional and usually wasteful.

The required set

CharacterEscapeWhy
double quote\"would end the string
backslash\\starts an escape sequence
newline\ncontrol character
carriage return\rcontrol character
tab\tcontrol character
backspace\bcontrol character
form feed\fcontrol character
U+0000 – U+001F\u00xxno short form for the rest

We checked the boundary rather than trusting it. Serialising the character at U+001F yields \u001f; the very next one, U+0020, is an ordinary space and passes through untouched. Everything from there upward may appear literally.

Worth noticing how short that list is. People expect JSON escaping to be fiddly and it is not — there are seven named escapes, one numeric form for the remaining control characters, and nothing else is required. The reason hand-built JSON breaks so often is not that the rules are complicated but that a single unescaped newline or backslash is enough, and both appear constantly in real data: multi-line text a user typed into a form, or a Windows path with backslash separators.

What must not be escaped

JSON.stringify("a/b")       // "a/b"      <- bare slash, not "a\/b"
JSON.stringify("한글 🎉")    // "한글 🎉"   <- literal UTF-8, no \u

The slash is optional. Both forms are valid and identical in meaning. The escape exists for one narrow reason: a literal </script> inside JSON embedded in an HTML page would close the script element early, so writing <\/script> prevents it. That is an HTML problem, not a JSON one.

Non-ASCII is optional too. JSON is defined over Unicode and the document is UTF-8, so Korean, emoji and accents belong in the string as they are. Some libraries default to \u escaping to produce ASCII-only output; turn that off unless something downstream actually needs it. Literal UTF-8 is smaller and readable in logs.

The distinction matters because unnecessary escaping is not free. An ASCII-only document with every Korean or emoji character expanded to\u sequences is roughly six times larger for that text than the literal UTF-8 form, and it is unreadable in a log or a diff. Some libraries still default to it for compatibility with transports that no longer exist in practice.

The emoji surrogate trap

"🎉".length                  // 2   <- one code point, two UTF-16 units
JSON.stringify("🎉").length  // 4   <- plus the surrounding quotes

JSON's \u form encodes a single UTF-16 code unit, so any character above U+FFFF requires two escapes rather than one. This is the same reason JavaScript reports a length of 2 for a single emoji.

It matters when you truncate. Cutting a string by character count can land between the two halves of a surrogate pair, leaving an unpaired unit that produces a replacement character or an outright invalid string. If you need to shorten user text before serialising, iterate by code point — the spread operator does this — rather than slicing by index.

There is a related habit worth dropping: truncating a payload for a log line by slicing the serialised string. That cut can land inside an escape sequence as easily as inside a surrogate pair, producing output that no longer parses and that looks, at a glance, like the original data was malformed. Truncate the value before serialising, or log the parsed object and let the logger handle it.

Do not build JSON by hand

Nearly every escaping bug comes from assembling JSON with string concatenation. A newline inside a value, a quote in a name, a backslash in a Windows path — each produces a parse error whose reported position points at the opening quote rather than the offending character, which makes it needlessly hard to find.

Pass your data to a serialiser and let it escape. It will get the rules above right every time, including the surrogate handling. If you are looking at a document that already fails, the formatter will report the parser's position, and JSON vs JSON5 covers the other syntax rejections that look like escaping problems but are not.

Frequently asked questions

Which characters must be escaped in JSON?

Seven have short forms — the double quote, the backslash, newline, carriage return, tab, backspace and form feed — and in addition every control character from U+0000 to U+001F must be escaped, using the \u form when no short form exists. We confirmed the boundary by serialising each: U+001F becomes \u001f while U+0020, the space, passes through untouched. Everything at U+0020 and above may appear literally. That is the entire rule, and it is shorter than most people expect, which is why hand-built JSON usually fails on a stray newline inside a string rather than on anything exotic. A Windows file path is the other frequent culprit, since every backslash in it needs doubling.

Do I need to escape the forward slash in JSON?

No. Both a bare / and an escaped \/ are valid and mean the same character, so this is purely optional — we confirmed that JSON.stringify emits the bare form. The escape exists because a literal </script> inside a JSON string embedded in an HTML page would end the script element early, and writing <\/script> avoids that. So the escape is an HTML-embedding workaround rather than a JSON requirement. If you are not putting JSON inside a script tag, leave slashes alone; if you are, escape the slash rather than relying on the consumer to cope — the alternative is an HTML parser ending your script element in the middle of a data blob.

Should I escape non-ASCII characters like Korean or emoji?

No, and doing so makes the payload larger for no benefit. JSON is defined over Unicode, and any character at U+0020 or above may appear literally as long as the document is UTF-8, so "한글 🎉" is valid exactly as written. The \u form remains available and some libraries default to it, usually to produce ASCII-only output for older transports. If you have that option, turn it off unless something downstream genuinely requires ASCII: literal UTF-8 is smaller, readable in logs, and avoids the surrogate-pair complications that \u escaping introduces for emoji. It is also what every browser and modern HTTP client expects by default, so the ASCII-only output mainly serves transports that no longer exist.

Why does an emoji need two \u escapes?

Because JSON's \u form encodes a single UTF-16 code unit, and characters above U+FFFF need two of them. An emoji is one code point but a surrogate pair in UTF-16, so escaping it produces two \u sequences rather than one — we measured "🎉" as length 2 in JavaScript and 4 characters when escaped. This is the same reason string length reports 2 for a single emoji. If you are splitting or truncating JSON strings by character count, a naive cut can land between the two halves of a pair and produce an invalid string, which is a real source of corrupted output. Iterating by code point with the spread operator avoids it, since that yields whole characters rather than code units.

How do I put a newline inside a JSON string?

Write \n, the two-character escape. A literal line break inside a string is a syntax error, which is the single most common failure when JSON is assembled by hand or built with string concatenation — the parser stops at the unterminated string and reports a position that points at the opening quote rather than the break. There is no multi-line string form in JSON; that is a JSON5 extension. The reliable answer is not to hand-build JSON at all: pass your data to a serialiser and let it handle escaping, which it will do correctly every time. Concatenation is the root cause of nearly every escaping bug in production JSON.

References

Paste a document into the JSON formatter to see exactly where a parser stops. More tools at withuse.io/tools.