What escaping is for
A JSON string is delimited by double quotes. So the moment the text inside needs a double quote of its own, there is a conflict: the parser would read that quote as the end of the string. Escaping resolves it by putting a backslash in front β \" means a literal quote character, not a delimiter.
The same trick covers everything else that cannot appear raw inside a string: the backslash itself, and the control characters below U+0020.
| Escape | Means |
|---|---|
\" | A double quote, not the end of the string |
\\ | One literal backslash |
\n | Newline |
\r | Carriage return |
\t | Tab |
\b \f | Backspace and form feed |
\/ | A forward slash. Legal, but never required |
\uXXXX | Any character by code point |
What does not need escaping
This is worth stating because over-escaping is as common as under-escaping. Non-ASCII text does not need to be escaped at all. JSON is Unicode, so accented letters, Chinese characters, and emoji are all valid raw inside a string.
- Forward slashes never need it.
https://example.comis fine as-is.\/is legal and some encoders emit it, which is why URLs in logs often look strange for no reason. - Single quotes never need it. They have no special meaning in JSON, so
\'is not a valid escape β it is an error. - Control characters always need it. A raw tab or newline inside a string is invalid JSON. They become
\tand\n; anything more obscure becomes\u0001-style.
Input JSON
{"s":"cafΓ© δΈζ π"}After escaping
"{\"s\":\"cafΓ© δΈζ π\"}"cafΓ©, δΈζ, and the emoji passed through untouched β \u00e9 would have been legal too, just unreadable.Control characters
A tab and a newline inside a string value cannot be written literally, so they come out as two-character escapes. This is the one case where escaping changes the byte count in a way people notice: a multi-line string gets visibly longer.
Input JSON
{"s":"a\tb\r\nc"}After escaping
"{\"s\":\"a\\tb\\r\\nc\"}"\t in the input was already an escape. After one round it is \\t β a backslash followed by a t, which is how a nested layer preserves it.Why the backslashes multiply
Here is the actual cause of the mess. A service logs a request body by putting the whole JSON document into a string field of another JSON document. The inner document's quotes now sit inside a string, so every one of them gets a backslash.
The payload
{"user":{"name":"Ada"},"ok":true}Stored as a JSON string
"{\"user\":{\"name\":\"Ada\"},\"ok\":true}"Two layers, and the doubling
Do it twice β a service logs a message that already contained a logged payload β and each backslash from the first round now needs escaping itself. One quote becomes \\\": an escaped backslash followed by an escaped quote.
This is why the count looks arbitrary. It is not: each layer roughly doubles the backslashes, so 1, 3, 7 backslashes means one, two, three layers of encoding.
One layer
"{\"user\":{\"name\":\"Ada\"},\"ok\":true}"Two layers
"\"{\\\"user\\\":{\\\"name\\\":\\\"Ada\\\"},\\\"ok\\\":true}\""Getting the payload back
Unescaping is the inverse, one layer at a time. Feed the escaped string in and you get the document back, formatted:
- One press removes one layer. A double-encoded string comes back as a still-escaped string, not as the document. Press again.
- The button is disabled when there is nothing to remove. It only lights up when the input parses as a string that itself parses as JSON β so a plain document cannot be mangled by an accidental click.
- Paste an escaped payload and the formatter notices. It says so rather than silently reformatting, because a string that happens to contain JSON and a JSON document are different things.
Escaped input
"{\"user\":{\"name\":\"Ada\"},\"ok\":true}"After removing escapes
{
"user": {
"name": "Ada"
},
"ok": true
}Avoiding it in the first place
Nested encoding is almost always accidental. If you control the code producing it, these are the fixes, in order of how much they help.
- Log structured, not stringified. Most loggers accept an object for a field.
logger.info({ body: payload })nests properly;JSON.stringify(payload)creates the layer you then have to peel. - Do not stringify before storing in a JSON column. Postgres
jsonband its equivalents take the value directly. Stringifying first stores a string, and every query against it needs a cast. - Never build JSON by concatenating strings. That is where under-escaping comes from β a quote in someone's surname ends the string early and the whole payload becomes invalid. Serialise with a library.
- Check message queue payloads. A body that is already JSON does not need re-encoding on the way into a queue that carries text.
Try it, or read further
- JSON formatterHas Escape and Remove Escapes in the toolbar, and detects escaped input on paste.
- JSON validation and SchemaThe other half of why a payload gets rejected.
- Can JSON have comments?Another thing the spec does not allow, and what to do about it.
- Complete JSON guideSyntax, types, and the rules behind the errors.