How to Escape JSON for HTML, SQL, Shell Commands and CSV
JSON escaping makes text safe inside JSON. Putting the JSON somewhere else needs that place's own escaping too. Here are the five places it goes wrong most often.
Escaping JSON correctly depends on where the JSON goes. The JSON Escape tool makes arbitrary text safe inside a JSON string. That is one layer. As soon as the finished JSON goes into another format, such as an HTML page, a SQL statement or a shell command, that format has its own rules, and valid JSON can break them.
Each example below was run, and each one uses the same kind of data that causes trouble in real systems: a name with an apostrophe, a quoted phrase, or text that a user typed.
Escape JSON inside a script tag
Server-rendered pages often pass initial data to the browser by writing JSON into a script tag. JSON.stringify does not escape < or /, so a value containing </script> ends the script element early, and whatever follows is parsed as HTML:
const data = { name: "Ada", bio: "</script><script>alert(1)</script>" };
const html = "<script>window.__DATA__ = " + JSON.stringify(data) + "</script>";
// An HTML parser finds two script elements here, and the second one runs.Escape < as the JSON escape \u003c. It is still valid JSON, still parses to the same string, and an HTML parser no longer sees a closing tag. Putting the data in a type="application/json" block instead of executable code is a good habit, but it is not a fix on its own: the HTML parser ends that block at </script> too.
const json = JSON.stringify(data).replace(/</g, "\\u003c");
const html = '<script type="application/json" id="initial-data">' + json + "</script>";
// In the browser:
const initial = JSON.parse(document.getElementById("initial-data").textContent);If your framework has its own way to pass server data to the page, use it; most apply this escaping for you. The risk is in hand-built templates.
Inside an HTML attribute
A data-config attribute with JSON in it has the opposite problem. Wrap it in single quotes by hand and the apostrophe in O'Brien ends the attribute: the browser reads the value as {"name":"O and treats the rest as junk. Set the attribute through the DOM instead, which escapes the quotes as " and reads back exactly:
element.setAttribute("data-config", JSON.stringify(config));
const config = JSON.parse(element.getAttribute("data-config"));On the server, let the template engine escape the attribute value rather than concatenating strings.
Inside a SQL statement
Building an INSERT by pasting JSON between single quotes breaks on the first apostrophe, and that is the harmless outcome. The harmful one is SQL injection, when the JSON comes from a user. In SQLite:
doc = json.dumps({"name": "O'Brien", "tags": ["vip"]})
db.execute("insert into customers (doc) values ('" + doc + "')")
# sqlite3.OperationalError: near "Brien": syntax error
db.execute("insert into customers (doc) values (?)", (doc,))
db.execute("select json_extract(doc, '$.name') from customers").fetchone()
# ("O'Brien",)Pass the JSON as a query parameter. The database driver sends it separately from the SQL text, so no quoting is needed and nothing in the value can change the statement. Every mainstream driver supports parameters; the placeholder is ?, $1 or :name depending on the driver.
Inside a shell command
The usual curl example wraps JSON in single quotes, which works until the data contains one:
curl -d '{"name":"O'Brien"}' https://api.example.com/customers
# The apostrophe closes the quoted string early. An interactive shell then
# waits for another quote; a script fails with "unexpected EOF".Three ways around it, all of which send the body unchanged:
# 1. Read the body from a file
curl --json @customer.json https://api.example.com/customers
# 2. A here-document with a quoted delimiter: no quote or $ is interpreted
curl --json @- https://api.example.com/customers <<'EOF'
{"name": "O'Brien", "note": "It's \"fragile\""}
EOF
# 3. Let jq build the JSON from shell variables
jq -n --arg name "$NAME" --argjson qty 2 '{name: $name, qty: $qty}' \
| curl --json @- https://api.example.com/customersTwo details. Plain -d sends the header Content-Type: application/x-www-form-urlencoded, which many APIs reject for a JSON body; --json, available since curl 7.82, sets application/json and also sends a POST. And jq's --arg is the safe way to put a shell variable into JSON: it escapes quotes, backslashes and newlines, which string concatenation does not.
Inside a CSV cell
A JSON object in a CSV column contains commas and double quotes, both of which mean something in CSV. RFC 4180 handles it by wrapping the field in double quotes and doubling every double quote inside. A CSV library does that for you:
id,payload
1,"{""name"": ""O'Brien"", ""tags"": [""vip"", ""eu""]}"Reading it back with the same library returns the original JSON text, ready for json.loads. Joining fields with commas by hand produces a file that splits the JSON across several columns.
Related tools
Turn a JSON-escaped string back into readable raw text.
Convert a JSON value into an escaped JSON string literal.
Pretty-print JSON with configurable indentation and instant validation.
Encode text or files to Base64, with an optional URL-safe variant.
Related guides
When a log line holds an object as an escaped string, you cannot search it by field. Here is how to read it today with jq, and the one-line change that stops it.
Most surprises from JSON.stringify are silent: a field disappears, a Map becomes {}, a date turns into a string. Here is exactly what happens to each kind of value, and how to control it.
Two formatters can disagree about the same JSON file. Here is how a team picks one, applies it in every editor and before every commit, and makes CI enforce it.
Base64 turns bytes into plain text so they can travel where only text is allowed. Here are the five places developers meet it most, with real examples, and the cases where it is the wrong tool.