JSON1

Syntax

JSON escaping

You pasted something out of a log file and it is unreadable β€” every quote has a backslash in front of it, and the braces are inside quotes. Nothing is broken. You are looking at a JSON document that was stored as a JSON string, and there is a mechanical way back.

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.

The complete set of escapes JSON defines. There are no others.
EscapeMeans
\"A double quote, not the end of the string
\\One literal backslash
\nNewline
\rCarriage return
\tTab
\b \fBackspace and form feed
\/A forward slash. Legal, but never required
\uXXXXAny 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.com is 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 \t and \n; anything more obscure becomes \u0001-style.

Input JSON

{"s":"cafΓ© δΈ­ζ–‡ πŸ˜€"}

After escaping

"{\"s\":\"cafΓ© δΈ­ζ–‡ πŸ˜€\"}"
Only the structural quotes gained a backslash. 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\"}"
The \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}"
The braces are now inside quotes: the whole document has become one string value.

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}\""
Counting backslashes before a quote tells you how many times to unescape. Three means twice.

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
}
Real output from the Remove Escapes button on the formatter.

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 jsonb and 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.