Unescape JSON: fix strings full of backslashes
· 3 min read · By Amir Badi
You expected {"id":1} and got "{\"id\":1,\"note\":\"hi\"}": the same data wrapped in quotes, with a backslash before every inner quote. Nothing is lost. The JSON was turned into a string one time too many, and one more parse gets it back.
The quickest fix for a one-off: paste the string into JSON Escape, choose Unescape, and the JSON comes back indented. For code, logs and pipelines, here's why it happens and how to undo it in JavaScript, Python and jq. Every command below was run with Node 22, Python 3.12 and jq 1.7.
Why JSON gets escaped twice
JSON.stringify turns an object into JSON text. Call it again on that text and you get a JSON string containing JSON:
const o = { id: 1, note: "hi" };
JSON.stringify(o); // {"id":1,"note":"hi"}
JSON.stringify(JSON.stringify(o)); // "{\"id\":1,\"note\":\"hi\"}"
The second result is correct JSON for a string. The backslashes are there so the inner quotes don't end it early. Real code rarely calls it twice on one line. It happens across layers:
- A web framework serializes the response for you, and the handler already passed it a string:
res.json(JSON.stringify(data))in Express, orjsonify(json.dumps(data))in Flask. - A field holds JSON text on purpose. Message queues, webhooks and event logs often carry a
payloadthat is itself serialized:
{"type":"order","payload":"{\"id\":7,\"total\":19.5}"}
- A logger writes a JSON object as a string field inside its own JSON log line.
If you own the code that produces it, the real fix is to serialize once: pass the object, not a string. If you're reading someone else's data, unescape it.
Unescape in JavaScript
Parse the outer JSON, then parse the string field:
const event = JSON.parse(text);
typeof event.payload; // "string"
JSON.parse(event.payload).total; // 19.5
If the whole document is a string, parse twice: JSON.parse(JSON.parse(text)).
Unescape in Python
Same idea with json.loads:
import json
event = json.load(open("event.json"))
type(event["payload"]).__name__ # 'str'
json.loads(event["payload"])["total"] # 19.5
Unescape with jq
jq's fromjson parses a string as JSON. Replace the field in place:
jq -c '.payload |= fromjson' event.json
{"type":"order","payload":{"id":7,"total":19.5}}
For a file that's one quoted string, jq 'fromjson' twice.json returns the object.
When the outer quotes are missing
Logs often print the inner text without its surrounding quotes:
{\"id\":1,\"note\":\"hi\"}
That isn't valid JSON on its own, so fromjson fails on it. Add the quotes back, parse it as a string, then parse the result. Put this in unquote.jq:
"\"" + . + "\"" | fromjson | fromjson
and run jq -Rc -f unquote.jq line.txt, which prints {"id":1,"note":"hi"}. (-R reads each line as raw text.) The JSON Escape tool does the same: in Unescape mode it accepts the text with or without the outer quotes.
Don't use find and replace
Deleting every backslash, or replacing \" with ", works on the example above and breaks on real data. A note containing a quote (She said \"hi\") or a Windows path (C:\\temp) has backslashes that belong to the inner string. Remove them blindly and the result is invalid JSON or changed values. A parser undoes exactly one level of escaping and nothing else.
What JSON actually requires you to escape
Inside a JSON string, only a few characters must be escaped:
| Character | Written as |
|---|---|
| Double quote | \" |
| Backslash | \\ |
| Line break, carriage return, tab | \n, \r, \t |
| Other control characters (U+0000 to U+001F) | \u0001 and so on |
Some things people expect to escape don't need it:
- Single quotes.
'is an ordinary character in JSON, and\'is an error.JSON.parserejects{"name":"O\'Brien"}with "Bad escaped character in JSON at position 11". If a single quote is giving you trouble, it's the shell, not JSON: in bash, write'{"name":"O'\''Brien"}'. - Slashes.
\/is allowed and reads back as/, but nothing requires it. PHP'sjson_encodewrites it by default;JSON.stringifydoesn't. - Accented letters and emoji. They can stay as they are. Python's
json.dumps("café ✓")writes"caf\u00e9 \u2713"unless you passensure_ascii=False, which gives"café ✓". Both parse to the same string.
Escaping on purpose
Sometimes you need the escaped form: JSON in an environment variable, a test fixture, a string field of another JSON document, or a template that expects text. Use JSON.stringify(text) in JavaScript or json.dumps(text) in Python, or paste it into JSON Escape with Escape selected. With JSON input it compacts the JSON first, so {"id": 1} becomes the one-line string "{\"id\":1}".
If the escaped JSON you're unpacking is broken (cut off by a log line limit, for example), unescape it first, then run what's left through JSON Repair. The guide to fixing invalid JSON explains the usual errors.