JSON String Escape / Unescape
Escape a string for embedding in JSON — and see what JSON.stringify leaves unescaped that will still break your page.
the escaped string appears here
What JSON.stringify gets right, and the two things it does not cover
JSON.stringify is the correct primitive for this and this page never
reimplements it: every standard escape, the well-formed lone-surrogate handling
ES2019 added (a bare \ud800 round-trips through JSON.parse
identically — worth confirming, not fixing), combining marks left untouched. What
it does not do is think about where the result is going. Measured on this machine
(Node 24.12): JSON.stringify("</script>") gives back
"</script>" — correct JSON, and the exact string that ends a
<script> block early if it is dropped straight into one. The
same call emits a literal U+2028 or U+2029 if the input has one — legal JSON,
invalid as a bare JavaScript string literal before ES2019, and still a genuine trip
hazard for anything on that spec version or older.
Embed-safe is the fix, and it is small on purpose: escape
/ as \/ (legal JSON per RFC 8259 §7, and inert inside
HTML — it can no longer spell a closing tag) and U+2028/U+2029 as
\u2028/\u2029. Nothing else changes; the result still
parses back to the exact original string.
Unescaping: where the errors actually are
JSON.parse's messages are better than most parsers' — they carry a
position — but they describe the grammar's rejection, not what the input actually
was. Measured against six pastes that land here constantly: \x41 is a
JavaScript or Python hex escape, not JSON's; \u{1F600} is the ES6
code-point escape, one character short of JSON's four-hex-digit
\uXXXX form; a value wrapped in 'single quotes' is a
JavaScript string literal; a literal tab or newline sitting unescaped inside the
quotes is usually a paste out of a formatted document; and a string that simply
never closes, or that has content after it closes, is a truncated or concatenated
paste. Six different mistakes, one V8 message between them
("Bad escaped character", mostly). This page names which one happened, in the
vocabulary of the tool that actually produced the input, and shows what decoded
successfully before the fault — a cut-off log field is still readable up
to the cut.
Most pastes here arrive without their wrapping quotes — someone copies a string's contents, not the JSON literal around them — so the page assumes them when they are missing and says so, rather than failing on input that is actually fine. None of this ever leaves the browser tab; the usual note applies here too (verify it yourself), though it is quieter on this page than most: a string being escaped for a curl body is often a token or a connection string, which is also why this page has no share-link button at all.
Do it without this tool
# Escape a whole file (or stdin) as one JSON string — -R reads raw lines, -s slurps them into one: jq -Rs . <<< $'line one\nline two' # "line one\nline two\n" # Reverse it — -r prints the *contents* of the JSON string, not a JSON-quoted one: echo '"line one\nline two\n"' | jq -r .
import json
json.dumps("café </script>")
# '"caf\u00e9 </script>"' ← Python's default, ensure_ascii=True, is the asciiOnly option here
json.dumps("café </script>", ensure_ascii=False)
# '"café </script>"' ← matches this page's default (asciiOnly off)
json.loads('"caf\u00e9"')
# 'café'
// Without embed-safe — JSON.stringify leaves / and U+2028/U+2029 alone:
const payload = JSON.stringify("</script><script>alert(1)</script>");
`<script>const data = ${payload};</script>`
// the literal </script> inside the string ends the block early — the alert
// runs as a second, attacker-controlled script tag, not as JSON data
// With embed-safe — legal JSON, and it can never close the tag around it:
const safe = payload.replace(/\//g, '\\/');
// "<\/script><script>alert(1)<\/script>" — JSON.parse gives back the original string
// A log field that was stringified twice — very common out of a JSON logging pipeline:
const field = '"{\\"user\\":\\"alice\\",\\"code\\":404}"';
const once = JSON.parse(field); // '{"user":"alice","code":404}' — still a string
const twice = JSON.parse(once); // { user: 'alice', code: 404 } — now an object
The specs, which one says what
RFC 8259 §7 is JSON's string grammar — the full set of escapes
that exist (\" \\ / b f n r t and \uXXXX) and the fact
that / is a permitted, not required, escape. The
well-formed JSON.stringify and
JSON superset TC39 proposals, both landed in ES2019,
are why a lone surrogate round-trips instead of becoming U+FFFD and why U+2028/U+2029
are legal inside a JavaScript string literal at all — before that, JSON's string
grammar was technically a JavaScript syntax error waiting to happen on those two
characters.
Three neighbours on the same workflow — escape this string for somewhere else: HTML entities for markup, percent-encoding for URLs, and base64 for bytes and files.
FAQ
Why does </script> inside a valid JSON string break my page?
The HTML tokenizer ends a <script> block on that exact byte
sequence, independent of JSON or JavaScript syntax. JSON.stringify
correctly leaves / unescaped, so embedding its output directly can
hand the browser a closing tag mid-payload. Embed-safe escapes the slash as
\/, legal JSON that cannot spell a tag.
What is U+2028 and why did it used to break JavaScript?
Both are valid JSON and valid JavaScript characters, but before ES2019 the
JavaScript grammar treated them as line terminators even inside a quoted string,
breaking any JSON payload embedded as a literal that happened to contain one.
Embed-safe escapes both to their \u2028/\u2029 spelling.
Why does \x41 fail to parse, and why the specific "bad escaped character" message?
\x41 is a JavaScript/Python escape; JSON's grammar (RFC 8259 §7) has
no \x form at all. V8 reports it as a generic "bad escaped character"
with no hint of which language it belongs to — this page names it.
Why does \u{1F600} fail, when \ud83d\ude00 for the same emoji works?
\u{1F600} is the ES6 code-point escape; JSON only has the
fixed four-digit \uXXXX form and represents an astral character as
two of them, one per surrogate half.
How do I read a JSON field that is itself a stringified JSON payload?
A JSON string whose own contents parse as JSON — a field stringified twice by two
layers of the same pipeline. The notes strip detects it and offers a second pass;
in code it is JSON.parse(JSON.parse(x)), one call per layer.
Is \/ required for a slash in JSON?
No — RFC 8259 §7 permits escaping / but never requires it, which is
why JSON.stringify never emits \/ on its own. It exists
precisely for the embed-safe case above.