Remove Invisible Characters From JSON

0 characters
Options

Aggressive mode rewrites emoji and right-to-left runs. For JSON payloads you usually want it off unless the text is prose.

Where they are

Every hidden character is marked in place, so you can see which key or string it was hiding in.

Nothing scanned yet.

U+200B always invisible — removed in default mode   U+200D invisible only in aggressive mode

Why invisible characters break JSON

Three failures come up over and over:

A byte order mark at the very start

A file saved as UTF-8 with BOM starts with U+FEFF. JSON.parse does not skip it, so the first thing it sees is not { or [ and it throws Unexpected token on line 1. The character is invisible, so the file looks like it starts with a bracket. It does not.

Zero width characters inside keys and values

A key that renders as name but contains U+200B is not the key name. The JSON parses fine, which is worse than an error: lookups silently miss, comparisons fail, and the two strings look identical in every log you print.

Tag characters and private use marks

Text copied out of a chat window or a web page can carry U+E0020–U+E007F tag characters or private use codepoints. They are valid in a JSON string, so nothing complains, and they travel with the data into whatever consumes it.

What writing the JSON does to them

Every invisible code point in this page's list is legal, unescaped, inside a JSON string, and every conforming parser hands it back unchanged — all twenty-three survive a round trip through JSON.parse and json.loads as the same code point. The trouble is not in the reading. It is in what the file looks like afterwards, and that depends entirely on which program wrote it.

Two writers, two different files

JSON.stringify escapes none of them: zero of twenty-three, all of them written as raw bytes. Python's json.dumps at its default escapes all twenty-three, so a zero width space leaves as the six characters \u200b. Add ensure_ascii=False and Python writes them raw as well.

CharacterJSON.stringifyjson.dumpsjson.dumps, ensure_ascii=False
U+200Braw, 3 bytes\u200b, 6 bytesraw, 3 bytes
U+200Draw, 3 bytes\u200d, 6 bytesraw, 3 bytes
U+FEFFraw, 3 bytes\ufeff, 6 bytesraw, 3 bytes
U+E0020raw, 4 bytes\udb40\udc20, 12 bytesraw, 4 bytes
U+0018\u0018, 6 bytes\u0018, 6 bytes\u0018, 6 bytes

The difference is not a rounding error. A 200-key object with one zero width space in each value is 3,981 bytes as Node writes it and 4,581 bytes as Python writes it with compact separators — the 600 bytes are exactly 200 × 3, one byte each for the backslash, the u and the extra hex digit. Switch ensure_ascii off and Python lands on 3,981 too, byte for byte the same file.

That choice decides whether you can see the dirt. In the Python file, grep -c '\u200b' finds it; in the Node file the same command returns nothing, and searching for the raw character gives the opposite pair of answers. A file that greps clean has not been cleaned — it may simply have been written by something that does not escape.

The one invisible character that must be escaped

Not everything invisible is a format character. The invisible thing that stops a JSON string dead is usually a C0 control: U+0018 sitting inside a word, U+0000 at the end of a value. Those are the only characters JSON requires you to escape, and both writers do — \u0018, six bytes standing in for one. Parsers reject the raw versions, and the two error messages point at the same spot without naming what is there: Bad control character in string literal in JSON at position 2 (line 1 column 3) from one, Invalid control character at: line 1 column 3 (char 2) from the other.

So a control character is the one invisible character that cannot hide inside valid JSON. If the file parses, it has been escaped; if you can read \u0018 in it, you have found your character. The other twenty-three can sit in a file that parses perfectly and leave no trace at all.

U+2028 is no longer the problem, </script> is

Line separator and paragraph separator are the characters people still warn about when JSON is dropped into a script block. That warning is out of date. A source file holding a raw U+2028 inside a JavaScript string literal parses cleanly in a current engine (checked with node --check), because the language adopted JSON's string rules in 2019. What still breaks out of a script block is </script>, and neither writer touches it: JSON.stringify returns it unchanged, forward slash and all. Escaping non-ASCII keeps old parsers safe and makes a stray line separator visible in a file, but it does nothing for the sequence that actually ends your script.

An unpaired surrogate is the only one that changes

Half an emoji — one surrogate code unit left behind by a bad slice or a truncated read — is accepted by both parsers. JSON.parse gives back the lone unit, and Python's json.loads gives back a one-character string. Then they disagree. Python refuses to encode it: 'utf-8' codec can't encode character '\ud83d' in position 0: surrogates not allowed. JavaScript writes it out as three bytes, EF BF BD, which is U+FFFD — the same file read back in Python now holds a replacement character, and whatever it originally was is gone.

Both writers escape it instead: JSON.stringify emits \ud83d, and Python emits the same six characters. Older engines emitted the raw half, which is valid in neither UTF-8 nor UTF-16 — that is what the escape rule fixed. Before a string goes anywhere, isWellFormed() returns false on it and true on ordinary text.

The byte order mark is the one thing nobody ignores

The current specification says a parser may ignore a byte order mark rather than treat it as an error. None of the ones measured here do. One reports Unexpected token; the other reports Unexpected UTF-8 BOM (decode using utf-8-sig) and, usefully, names the fix. Opening the file with encoding='utf-8-sig' reads it without a cleanup step.

Tick Escape non-ASCII in the options above and this page writes the escaped form instead, then shows both byte counts, so you can see the difference on your own payload.

How to find them in your editor

In VS Code, open the file, press Ctrl+F, turn on the regular expression button and search for:

[\u200B\u200C\u200D\u00AD\u2060\uFEFF\u034F]

Every hit is a character that should almost certainly not be there. In Vim or Neovim the same pattern works with /. On the command line, grep -P '[\x{200B}\x{200C}\x{200D}\x{00AD}\x{2060}\x{FEFF}]' finds them in a file.

What this page does and does not do

It removes invisible characters and then runs JSON.parse on the result, so you find out whether the cleanup was enough. That is the whole job.

It does not pretty-print, minify, sort keys or repair syntax. A missing comma is still a missing comma. If the panel says the JSON is invalid after cleaning, the remaining problem is real JSON syntax, not a hidden character.

Two characters worth knowing about

U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are legal inside a JSON string but were illegal in JavaScript source before ES2019, so they break the common pattern of dropping JSON straight into a <script> tag. They also look like ordinary line breaks. This page leaves both alone, because in a JSON string they may be exactly what was intended.

Is anything uploaded?

No. There is no backend. The cleanup and the validation both run in this browser tab, and your JSON never leaves your device.