If you are looking at JSON full of backslashes before every quote, you almost certainly have a double-encoded string: a JSON document that was serialised to text, and then that text was serialised again as a value inside another JSON document.
The instinct is to reach for an unescape tool and strip the backslashes. That works once and does not fix anything, because the same thing will arrive tomorrow. The useful version is understanding which of the several causes produced it, because each has a different fix at the source.
Table of contents
- What escaping is actually doing
- Double encoding, which is the actual problem
- Unescaping it properly
- Stopping it at the source
- How this fits the rest of the stack
- FAQ
What escaping is actually doing
JSON delimits strings with double quotes, so a double quote inside a string has to be marked as data rather than as the end of the string. The backslash does that.
The full set of escapes is small:
\"— a double quote\\— a literal backslash\n,\r,\t— newline, carriage return, tab\b,\f— backspace, form feed\/— a forward slash, which is optional and rarely necessary\uXXXX— any character by its code point
That is the entire list. \' is not valid JSON — single quotes need no escaping — and a parser will reject it. This is one of the most common hand-authoring mistakes, usually from someone applying JavaScript string habits to a JSON file.
Normal single-level escaping is invisible in practice, because your parser handles it. {"msg": "She said \"hi\""} parses to a string containing She said "hi", and you never see the backslashes. Seeing them means something did not parse when it should have.
Double encoding, which is the actual problem
Here is the shape of it. You have an object:
{"name": "Ada", "role": "engineer"}
Serialised to a string, that is {"name": "Ada", "role": "engineer"}. Now put that string as a value inside another JSON object, and the quotes inside it must be escaped:
{"payload": "{\"name\": \"Ada\", \"role\": \"engineer\"}"}
That is double-encoded. payload is a string that happens to contain JSON, not a nested object. The consumer has to parse twice — once for the outer document, once for the string inside it — and if they only parse once they get a string where they expected an object.
Where this comes from, in rough order of frequency:
- A field typed as string in a schema or database column that is being used to hold structured data. Every write serialises, every read returns a string.
- Serialising something that was already serialised — calling a to-JSON function on a value that was already a JSON string. Very easy in loosely-typed code.
- Logging or message-queue wrappers that put your payload into an envelope as a string field.
- Environment variables, which are always strings, so any structured config in one is JSON-in-a-string by definition.
- Webhook providers that wrap the event body as a string field for compatibility.
Note that the last two are legitimate and not bugs. An environment variable genuinely cannot hold an object. The problem is only when double encoding is accidental, or when a consumer does not know it is there.
Unescaping it properly
The correct operation is to parse twice, not to strip backslashes with a regular expression.
// JavaScript
const outer = JSON.parse(raw); // outer.payload is a string
const data = JSON.parse(outer.payload); // now it is an object
console.log(data.name); // "Ada"
# Python
import json
outer = json.loads(raw) # outer["payload"] is a str
data = json.loads(outer["payload"])
print(data["name"]) # Ada
Parsing is right and regex is wrong for a specific reason: a regular expression that removes backslashes cannot tell a \\ that represents one literal backslash from an escape of something else, and it will corrupt any string containing Windows paths, LaTeX, or a regular expression of its own. Parsing handles all of that correctly by construction.
If you genuinely do not know how many levels deep it goes — which happens with layered systems — loop, but bound it:
def deep_parse(value, limit=5):
for _ in range(limit):
if not isinstance(value, str):
return value
try:
value = json.loads(value)
except json.JSONDecodeError:
return value # a plain string, not more JSON
return value
The bound matters. Without it, a string like "5" parses to the number 5, and a malformed input can send an unbounded loop somewhere unhelpful.
Stopping it at the source
Unescaping in the consumer is a workaround. The fixes worth making:
- Use a JSON column type where your database has one —
jsonbin Postgres,JSONin MySQL. The value is stored as structured data rather than text, queryable and impossible to double-encode by accident. - Check whether a value is already a string before serialising it. The single most common source in application code.
- Fix the API contract. If you control both ends, send the object as a nested object. A field documented as an object should be an object.
- Accept it where it is unavoidable — environment variables, some webhook envelopes — and parse deliberately at the boundary, in one place, rather than scattering parse calls through the codebase.
- Validate against a schema at the boundary, which catches a string where an object was expected at the point it arrives rather than five functions later.
One extra caution for environment variables specifically: shell quoting will fight you. A JSON value in a shell-exported variable needs single quotes around it to stop the shell interpreting the backslashes, and a .env file parser may apply its own rules on top. If a config value arrives mangled, test what the process actually received rather than what the file appears to contain.
How this fits the rest of the stack
The durable fix for most double-encoding is a real JSON column rather than a text field, which is a database capability rather than an application workaround. The RunxBuild hosting calculator shows the database beside the service, storage and bandwidth. RunxBuild runs managed MySQL and Postgres — both support native JSON column types, with Postgres offering jsonb and its indexing — alongside services that read configuration from environment variables injected at runtime rather than committed.
Useful related references:
- XML to JSON: The Conversions That Are Not Actually Lossless
- Should package-lock.json Be Committed?
- JSON to YAML: Tools, When to Convert, and the Gotchas
- Services on RunxBuild
FAQ
Why does my JSON have backslashes before every quote?
It is double-encoded — a JSON document was serialised to a string, and that string was then stored as a value inside another JSON document. The quotes inside the inner document have to be escaped. You are looking at a string that contains JSON rather than a nested object.
How do I unescape a JSON string?
Parse it twice rather than stripping backslashes. Parse the outer document, then parse the string field inside it. A regular expression that removes backslashes cannot distinguish an escaped backslash from other escapes and will corrupt Windows paths, regexes and similar.
What causes double-encoded JSON?
Most often a schema or database column typed as string being used to hold structured data, or code calling a serialise function on a value that was already serialised. Environment variables and some webhook envelopes are double-encoded by necessity rather than by mistake, since they can only hold strings.
Is backslash-apostrophe valid in JSON?
No. Single quotes need no escaping in JSON, and a parser will reject the sequence. It is a common hand-authoring mistake from applying JavaScript string habits to a JSON file. The valid escapes are the quote, backslash, the whitespace set, forward slash, and unicode code points.
How do I stop producing double-encoded JSON?
Use a native JSON column type where your database offers one, so the value is stored as structured data rather than text. In application code, check whether a value is already a string before serialising it. Where double encoding is unavoidable, parse deliberately at one boundary rather than scattering parse calls.