JSON strings are sequences of Unicode characters, and the format gives you two ways to write any character above ASCII: literally, or as a \uXXXX escape. Both produce exactly the same value.
JSON.parse('"\u00e9"') // "é"
JSON.parse('"é"') // "é"
// identical — same string, same length, same bytesWhich means the escape is never *required* for correctness. It is a transport convenience, for when you cannot trust everything between you and the reader to handle UTF-8.
The emoji
Here is the thing that catches everyone:
const s = JSON.parse('"😀"');
s.length // 2
Buffer.byteLength(s) // 4
[...s].length // 1Three different answers for one emoji, and all three are correct — they measure different things.
Characters above U+FFFF — emoji, many CJK extensions, historic scripts, mathematical symbols — are stored as a surrogate pair: two code units that mean one character only when read together. JSON can write them either way:
JSON.parse('"\ud83d\ude00"') // "😀" — the surrogate pair, escaped
JSON.parse('"😀"') // "😀" — the same thing, literallyLone surrogates
Half a surrogate pair is not a valid Unicode character, but JSON will carry it and JavaScript will hold it:
const half = JSON.parse('"\ud83d"');
half.length // 1
JSON.stringify(half) // '"\ud83d"' — escaped on the way outModern JSON.stringify emits lone surrogates as escapes rather than raw bytes, which is what makes that round trip survive. It is deliberate: a raw lone surrogate cannot be encoded as valid UTF-8, so a literal would produce a byte sequence no conforming decoder accepts.
Where they come from, almost always: a string truncated by code unit rather than by character. Cut "😀" at one unit and you have a lone high surrogate. If you truncate user text anywhere, truncate by code point.
The escapes JSON defines
Every character below U+0020 must be escaped. A literal newline inside a string is a parse error, which is why a multi-line value arrives as "line one\nline two".
The slash escape nobody needs
\/ is legal and means exactly /:
JSON.parse('"slash\/escaped"') // "slash/escaped"It exists for one reason: so JSON can be embedded in an HTML <script> block without the sequence </script> inside a string ending the block early. Writing <\/script> avoids it.
Outside that specific case it is noise. Some older serialisers escape every slash, which is why URLs in JSON from certain PHP backends arrive as http:\/\/example.com — valid, equal to the unescaped form, and merely ugly. Re-serialising with the formatter normalises it away, because a parser produces the same string either way and JSON.stringify does not add the escape back.
Practical rules
- Send UTF-8 and say so.
Content-Type: application/json; charset=utf-8. JSON is Unicode by definition and UTF-8 is the default encoding. - Escape only when the path is untrustworthy. If something between you and the reader mangles non-ASCII,
\uXXXXis pure ASCII and survives anything. - Measure in code points, not code units, whenever a human-facing limit is involved — character counters, truncation, column widths.
- Check your database column type. MySQL
utf8is not UTF-8; it is three bytes per character and cannot store an emoji at all.utf8mb4is the real one. - Never hand-escape. Use your language's serialiser; it knows the rules in the table above and you will not.
If a document is failing to parse and non-ASCII is involved, the validator gives the line, the column and the specific rule broken — usually a raw control character or a backslash that is not a valid escape.