Fixing JSON Syntax Errors in JSON-LD
A JSON-LD block is only useful if a machine can read it, and JSON is unforgiving: one unescaped quote, a comma after the last item or a raw line break inside a value, and the parser rejects the entire block.
Nothing looks wrong to visitors, but search engines and AI systems see no structured data at all, and Search Console may list the page under unparsable structured data. This guide shows how to pinpoint the exact character that breaks the parse, trace it back to the template or editor that produced it, and stop it happening again.
What one bad character does
A browser treats a <script type="application/ld+json"> element as inert data. It
never runs it and never complains about it, which is why a broken block can sit on a live site for months.
The consumers that do read it, such as Googlebot, Bing, social platforms and AI crawlers, pass the text
to a JSON parser. JSON parsers are all-or-nothing: they either return a complete object or they return
an error and nothing else.
The practical effect is wider than people expect:
- The whole script element is lost, not just the property with the mistake. If your
Organization, WebSite, BreadcrumbList and Product all live in one
@graph, a stray quote in the product description removes all four. - Other script elements survive. Each block is parsed separately, so a broken review-app block does not take the theme's block down with it. This is one reason tools sometimes report "some" structured data on a page that should have more.
- Rich result tests describe it vaguely. Google's Rich Results Test and Search Console group these under "Unparsable structured data", often with a short message such as a missing comma or a bad escape, but without showing you the template line that caused it.
Because the failure is silent, syntax problems are usually found by accident, after a drop in rich results or a report from an audit tool. The JSON Checker parses every block on the page and reports the syntax first, because nothing else in a block can be checked until it reads cleanly.
Parser messages and what they really mean
Different parsers word the same fault differently. These are the messages you are most likely to meet, with the usual cause behind each.
| Message (typical wording) | Usual cause | What to look at |
|---|---|---|
| Expected double-quoted property name / Expecting property name enclosed in double quotes | A comma after the last property, or a key written in single quotes or with no quotes. | The character
just before the reported position, usually a , followed by }. |
| Expected ',' or '}' after property value / Expecting ',' delimiter | A double quote inside a value ended the string early, or a comma is missing between two properties. | The last string
value before the position: look for " characters inside text such as measurements or quoted
phrases. |
| Bad control character in string literal / Invalid control character | A raw line break or tab inside a string, usually from a multi-line description field. | Descriptions, addresses and review bodies pulled from text areas. |
| Unexpected token / Unexpected character | Curly quotes, a comment, a lone undefined
or NaN, or a template tag that was not processed. | Non-ASCII quote marks and leftover
template syntax such as {{ }} or <?php. |
| Unexpected end of JSON input / Unterminated string | The block was cut short: a closing
bracket is missing, or the content contains </script> which ended the element early. | The end of the block and any HTML stored in string values. |
| Extra data / Unexpected non-whitespace character after JSON | Two objects written one after the other with no array around them, or content after the final brace. | Blocks built by joining several snippets together. |
| Invalid \escape / Bad escaped character | A backslash followed by a character JSON does not allow, often from Windows paths or regular expressions pasted into text. | Any \
in a value that is not followed by ", \, /, b, f,
n, r, t or u. |
{ or [, so
the numbers line up with what you are looking at.Pinpointing the fault in a few minutes
Look at the served HTML, not the Elements panel
The Elements panel in developer tools shows the page after scripts have run and after the browser
has tidied the markup. For JSON-LD you need the bytes the server actually sent, which is what a crawler
receives first. Use view source (Ctrl+U, or Cmd+Option+U on a Mac), or fetch the page from the command
line with curl -s https://www.example.co.uk/page/ > page.html. If a tag manager or JavaScript
adds the block later, check the rendered version too, since that is a separate source of errors.
Parse every block in one go from the console
Open the page, open the console, and paste this. It tries each block and prints the error for any that fail, together with the text around the failure point.
document.querySelectorAll('script[type="application/ld+json"]').forEach((el, i) => {
const text = el.textContent;
try { JSON.parse(text); console.log('Block ' + i + ': OK'); }
catch (e) {
const m = /position (\d+)/.exec(e.message);
const p = m ? Number(m[1]) : 0;
console.log('Block ' + i + ': ' + e.message);
console.log(text.slice(Math.max(0, p - 80), p + 20));
}
});
This checks the rendered DOM, so it catches blocks that scripts inject, but it will not show HTML entities exactly as the server sent them. If the console reports every block as fine and a crawler still says the markup is unparsable, compare against view source.
Parse a saved block on the command line
Save the block's contents, without the script tags, to a file and run python3 -m json.tool block.json
or jq . block.json. Python reports a line and column, which is easier to follow in an editor
than a character position. Turn on "show invisible characters" or "render whitespace" in your editor
as well: non-breaking spaces, zero-width characters and byte order marks are invisible but matter.
Where broken JSON comes from
Almost nobody types invalid JSON by hand into a finished page. It is produced by one of a handful of processes, and finding which one tells you where the lasting fix belongs.
- Templates that glue strings together. A template that writes
"name": "{{ title }}"works until a title contains a double quote, a backslash or a line break. This is the single biggest cause, and it affects every page that uses the template. - Loops that leave a trailing comma. A loop that prints
"x",for every item leaves a comma after the last one. So does a list of optional properties where the final one is skipped because it is empty. - Empty values printed raw.
"price": {{ price }},becomes"price": ,when the price is missing. Numbers and booleans have no quotes around them to hide the gap. - Rich text editors. Content pasted from a word processor brings curly quotes, non-breaking spaces and soft line breaks. Some page builders also convert straight quotes to curly ones when you save a custom HTML block.
- Copied examples. Documentation often shows JSON with
//comments,...placeholders or single quotes for readability. Pasted as-is, they fail. - Optimisation plugins and minifiers. Tools that combine, minify or defer inline scripts sometimes treat a JSON-LD block as JavaScript and rewrite it, or add attributes that change its type. If a block validates in your template but not on the live page, switch the optimiser off for that element and test again.
- HTML stored in values. A description containing
</script>ends the element early, cutting the JSON in half. A description with<a href="...">breaks the string at the first quote if it is not escaped.
Encoding values properly, by language
The durable fix is the same everywhere: build the data as a native object or array, then turn the whole thing into JSON with the language's own encoder. Encoders escape quotes, backslashes and control characters, never leave trailing commas and write numbers correctly.
| Environment | Use | Note |
|---|---|---|
| PHP | json_encode($data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG) | JSON_HEX_TAG
turns < and > into \u003C and \u003E, so stored
HTML cannot close the script element. |
| WordPress | wp_json_encode( $data ) | Handles character set problems
that plain json_encode can fail on. Hook it into wp_head rather than pasting
raw JSON into a theme file. |
| Shopify Liquid | {{ product.title | json }} | Outputs the value with its surrounding quotes, so do not add your own. |
| JavaScript / Node | JSON.stringify(data).replace(/</g, '\u003c') | The
replace step protects against </script> inside values when you write the result into
HTML. |
| Python | json.dumps(data, ensure_ascii=False) | Then replace <
with \u003c before placing it in a template, and mark the output as safe so the template
engine does not HTML-escape it. |
| Twig | {{ data|json_encode|raw }} | Without raw, Twig escapes
the quotes into entities and the block breaks. |
Before and after
This fragment was built by joining strings. The inch marks in the product name close the string early and the loop left a comma after the final property:
{
"@type": "Product",
"name": "Shelf bracket 6" black",
"sku": "SB-6-BLK",
}
The same data passed through an encoder produces a block that parses:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Shelf bracket 6\" black",
"sku": "SB-6-BLK",
"description": "Powder-coated steel bracket.\nSupports shelves up to 20 cm deep."
}
</script>
Notice the escaped quote after the 6 and the \n in place of a real line break. You would
never type these by hand; the encoder writes them for you.
" reaches the parser as five literal
characters and breaks the block.The fix, step by step
Step 1
Pull the exact block the server sends
Open view source on the affected page, or save it with curl, and copy each
application/ld+json block from its first bracket to its last. Do not copy from the Elements
panel, because the browser may have reformatted it. Note which page template the URL uses and whether
the block sits in the head or body, because that position often hints at which theme file, plugin or
app printed it.
Step 2
Run it through a strict parser
Run
the page through the JSON Checker, then paste the failing block into python3 -m json.tool,
jq or the browser console with JSON.parse. Use a strict parser rather than
a lenient online formatter, because some formatters silently repair trailing commas and single quotes
and then tell you the JSON is fine. Keep the exact error message and the reported line, column or character
position for the next step.
Step 3
Read the error position backwards
Go to the reported position and then read to the left. A parser only notices a problem when it meets a character it cannot accept, so the real mistake is usually earlier: an unescaped quote in the previous value, a missing comma at the end of the previous line, or a bracket that was never closed. Turn on invisible characters in your editor to expose curly quotes, raw tabs and non-breaking spaces.
Step 4
Trace the bad character to its source
Decide where the faulty text came from: a product or post field, a theme template, an SEO
plugin, an app, a tag manager or a custom HTML block. Search your theme files for ld+json,
disable plugins one at a time on a staging copy, or compare the block with the admin field it came from.
Fixing the single page by hand helps nobody if the same template breaks the next page tomorrow.
Step 5
Replace string building with a JSON encoder
Rewrite the template so it builds an object or array and prints it once with json_encode,
wp_json_encode, the Liquid json filter or JSON.stringify. Leave
out properties whose values are empty instead of printing blank numbers. If the block comes from a plugin
or app you cannot edit, clean the source field instead, report the fault to the vendor, and switch that
output off if it keeps breaking.
Step 6
Add a parse check before every deploy
Add a small script to your build or deployment that extracts every application/ld+json
block from the generated pages and parses it, failing the build on any error. Then re-run the JSON Checker
on one page of each template, and in Search Console use Validate fix on the unparsable structured data
report so Google rechecks the affected URLs as they are recrawled.
Cases that catch people out
- Valid JSON that says less than you think. JSON allows the same key twice, and most
parsers quietly keep the last one. A block with two
"image"keys parses without complaint, but the first value is gone. The JSON Checker reports duplicate keys because a parser will not. - Numbers with leading zeros.
"postalCode": 01234is invalid; a leading zero is not allowed in a JSON number. Postcodes, phone numbers and SKUs should always be strings. - Old CDATA wrappers. Some older themes wrap inline scripts in
//<![CDATA[and//]]>. That is harmless in JavaScript but breaks JSON, because JSON has no comments. - A byte order mark. A file saved with a byte order mark can put an invisible character before the first brace. Some parsers reject it. Re-save the file as UTF-8 without a BOM.
- Two objects side by side.
{...}{...}is not valid. Wrap them in an array,[{...},{...}], or put both in one@graph. - Blocks added by a tag manager. A custom HTML tag that injects JSON-LD may be editing text with its own variable syntax. Check the rendered output, not the tag's editor, because the variables are substituted at run time.
Catching it before it ships
A parse check is cheap and catches almost all of these faults before a visitor or crawler sees them. This Python script reads every HTML file in a build folder and fails if any JSON-LD block does not parse:
import json, pathlib, re, sys
bad = 0
pattern = re.compile(r'<script[^>]*application/ld\+json[^>]*>(.*?)</script>', re.S | re.I)
for page in pathlib.Path('public').rglob('*.html'):
for n, block in enumerate(pattern.findall(page.read_text(encoding='utf-8'))):
try:
json.loads(block)
except json.JSONDecodeError as e:
bad += 1
print(f'{page} block {n}: {e}')
sys.exit(1 if bad else 0)
On a CMS without a build step, run the same check against a list of live URLs each week, one per template, and after every theme, plugin or app update. Train editors not to paste formatted text into fields that feed structured data, and prefer plain text fields for product names, headlines and summaries. For what to fix once every block parses, the missing required fields guide is the natural next stop.
1. Why does my JSON-LD work in one validator and fail in another?
Some online tools are deliberately lenient. They repair trailing commas, accept single quotes or strip comments before parsing, then report success, while search engines use strict parsers that reject the same text. Other differences come from what the tool fetched: one may read the raw HTML the server sent, another the rendered page after scripts ran. Always test with a strict parser and compare view source with the rendered DOM when results disagree.
2. Does a syntax error in one block break all the structured data on the page?
No, only the script element that contains the error. Each JSON-LD block is parsed on its own, so a broken block from a review app does not remove the valid block your theme prints. The catch is that many sites put everything into one block with a graph, in which case a single bad character removes every entity at once. That is a good reason to fix syntax errors before anything else on the report.
3. Can JSON-LD contain comments?
No. JSON has no comment syntax at all, so double slashes, slash-star blocks and HTML comments inside the script element all cause parse errors. If you need to leave notes for other developers, put them in the template language outside the output, for example a Liquid comment tag or a PHP comment, so they never reach the page. Some documentation examples include comments for explanation, so remove them before pasting any sample into a site.
4. How do I include a double quote inside a JSON-LD value?
Escape it with a backslash, so a six inch shelf becomes 6 followed by backslash and quote, or reword the value to avoid the mark, for example 6-inch. Doing this by hand is error-prone, so the better answer is to let an encoder do it: json_encode in PHP, the json filter in Liquid or JSON.stringify in JavaScript all escape quotes automatically. Never replace the quote with an HTML entity, because entities are not decoded inside script elements.
5. Why does Search Console say unparsable structured data when my page looks fine?
Visitors never see JSON-LD, so a broken block has no visible effect on the page. Search Console reads the code, not the design, and reports any block its parser rejected. Open the report, pick an example URL and test it live with URL Inspection or the Rich Results Test to see the message. Then parse the served HTML yourself to find the exact character, because the report rarely names the template that caused it.
6. Is it safe to fix the JSON directly in the page HTML?
Only as a stopgap. If the block is printed by a template, a plugin or an app, your manual edit is overwritten the next time the page is saved, the cache is cleared or the software updates, and every other page using the same template remains broken. Find the template or setting that generates the block and fix it there. Hand edits make sense only for genuinely hand-written blocks on one-off pages such as a contact page.
7. Do HTML minifiers or caching plugins break JSON-LD?
Most handle it correctly, but some optimisation tools treat every inline script as JavaScript, then combine, defer or rewrite it. That can strip line breaks inside strings, change the type attribute or move the block so it no longer parses as intended. If the block is valid in your template output but invalid on the live page, exclude application/ld+json scripts from minification or deferral in the tool settings and test again after clearing the cache.
The JSON Checker parses every JSON-LD block on your page, validates the syntax, checks each type against schema.org, verifies required fields, detects conflicting types and runs the result against Google rich result criteria.
Open JSON-LD Fixes โ