U+2028 and U+2029 in JSON and JavaScript: what actually breaks

JSON.parse does not break. Measured on Node 22, current Chromium and Python 3.13: a raw U+2028 or U+2029 inside a JSON string parses cleanly in all three. What actually differs is the serializers — JavaScript writes these characters out raw, Python escapes them. And inside JavaScript source code, both characters still count as line terminators, which is where the real bugs come from.

Measured, one row at a time

Each row is a single expression evaluated on Node 22. U+2028 is the line separator, U+2029 the paragraph separator; both are invisible on screen.

Where the character sitsResult
Inside a JSON string, parsed with JSON.parseOK — parses fine
Inside a JS string literal ("x
y")OK
Inside a template literalOK
Between two statements, acting as whitespaceOK
Inside a // comment, with code after itEnds the comment — the code after it runs
Inside a regex literalSyntaxError: Invalid regular expression

The last two rows are the ones that bite. A U+2028 inside a line comment terminates the comment, so whatever was meant to be commented out starts executing. A U+2028 inside a regex literal stops the literal from parsing at all. Neither of these looks like anything when you read the file, because the character has no width.

The serializers disagree

This is the part that catches people out across language boundaries. Same object, two languages:

// JavaScript, Node 22
JSON.stringify({a: "x
y"})
// -> {"a":"x
y"}         raw character, NOT escaped

# Python 3.13
json.dumps({"a": "x
y"})
# -> {"a": "x\u2028y"}         escaped

Python escapes them; JavaScript does not. So JSON produced by a JS service carries the raw character across the wire, while JSON produced by a Python service carries a six-character escape sequence. Both parse fine on the receiving end — until the payload is pasted into a JavaScript file instead of parsed, and the line-terminator behaviour above takes over.

The one-line fix when you generate JS from data

If you are writing JSON into a <script> tag or building JavaScript source from data, escape these two characters explicitly. JSON.stringify will not do it for you:

const safe = JSON.stringify(data)
  .replace(/\u2028/g, "\\u2028")
  .replace(/\u2029/g, "\\u2029");

The output still parses to the same value — verified by round-tripping it back through JSON.parse — but it now survives being pasted into source code.

Checking whether you have them

Both characters are invisible, so the only way to know is to look. The zero-width space remover shows you where invisible characters sit in a block of text, and the invisible characters list has the code points. For a payload that will not parse at all, start with the JSON cleaner.

What this page does not claim

These measurements come from Node 22, current Chromium and Python 3.13 — the runtimes available here. Older JavaScript engines treated U+2028 and U+2029 as line terminators everywhere, including inside string literals, which is where this character's reputation comes from; that was relaxed in ES2019, and I could not test pre-2019 engines on this machine, so this page makes no claim about them. Run the snippets above against the runtimes you actually ship to.