Skip to Content

Invalid JSON-LD that search engines will discard

What Is This Issue

A <script type="application/ld+json"> block on the page does not contain valid JSON, so search engines discard it entirely.

Structured data is all-or-nothing per block. A single trailing comma, an unescaped quote inside a string, or a stray comment is enough for the parser to reject the whole script — along with every schema it contained. The markup is present in the HTML, it looks correct in the page source, and it does nothing.

This is the most damaging structured-data failure precisely because it is invisible: a missing schema is at least obvious once you look for it, whereas a malformed one appears to be there.

Common causes:

  • A trailing comma after the last property of an object or array
  • Unescaped double quotes inside a value — a product name like 12" Monitor
  • Template placeholders that were never substituted
  • HTML entities left encoded inside the JSON (&quot;, &amp;)
  • JavaScript syntax that is not JSON: single quotes, unquoted keys, // comments
  • Concatenated output from a CMS that emits two objects with no enclosing array

Why Is This Important

An invalid JSON-LD block is worth exactly as much as no JSON-LD block, and costs more, because everyone involved believes the schema is in place.

  • No rich results. Google cannot read the block, so no breadcrumbs, star ratings, FAQs, prices or event details appear in search. The listing renders as plain blue-link markup.
  • Silent failure. Nothing warns you. The page renders normally, the block is there in view-source, and Search Console reports the schema as absent rather than broken — so the natural conclusion is “we need to add schema” when it is already there.
  • Everything in the block is lost. Most sites put several schemas in one script. One syntax error takes out Organization, WebSite and BreadcrumbList together.
  • AI answer engines skip it too. Assistants that read structured data to describe a page fall back to guessing from the prose.
  • It survives releases. Because it never raises an error, an invalid block introduced by a template change can sit in production for months.

How XeoPix Detects This

  1. Fetches the page and parses the raw HTML.

  2. Collects every JSON-LD block: each <script type="application/ld+json"> element, in document order.

  3. Parses each block independently with a strict JSON parser. A block either parses or it does not; there is no partial recovery, which is the same all-or-nothing behaviour search engines apply.

  4. Records the parser error for any block that fails — the message and position the parser reported, along with the block’s index on the page.

  5. Raises one finding per failing block, so a page with two broken scripts reports two findings rather than one aggregate. The block index in the message tells you which <script> to open.

A block that parses but describes an incomplete schema is a different finding: the per-schema checks (Product, Article, FAQPage, and so on) cover those. This issue is only about markup a parser rejects outright.

Note: detection uses raw HTML only — no JavaScript is executed. Structured data injected client-side after load cannot be inspected here, and Google’s own treatment of it is unreliable, so server-rendering JSON-LD is recommended regardless.

How To Fix

  1. Find the failing block. The issue detail names which script block on the page failed and quotes the parser error, e.g. Invalid JSON-LD syntax in script block 2: Unexpected token } in JSON at position 412.

  2. Validate it. Paste the block’s contents into the Schema Markup Validator , or run it through JSON.parse() in a browser console. Both report the exact character position.

  3. Fix the syntax. In roughly the order each one turns out to be the culprit:

    • Remove the trailing comma before a closing brace or bracket.
    • Escape double quotes inside string values as \".
    • Replace single quotes with double quotes — JSON has no single-quoted strings.
    • Quote every key. {"name": "..."}, never {name: "..."}.
    • Delete // and /* */ comments. JSON has no comments.
    • Wrap multiple top-level objects in an array, or combine them under one @graph.
  4. Fix the generator, not the output. A hand-patched page breaks again on the next content edit. Build the object in code and serialise it — JSON.stringify(schema) in JavaScript, or the equivalent JSON filter in your template language. Never assemble JSON-LD by concatenating strings in a template, which is where almost all of these come from.

  5. Escape for the HTML context. When injecting JSON into a <script> tag, replace < in the serialised output with < so a value containing a closing script tag cannot end the block early.

  6. Add a build-time check. Parse every JSON-LD block in your rendered output as part of CI. This class of bug is trivial to catch automatically and almost impossible to catch by eye.

  7. Re-scan to confirm the block now parses.

What We Store

For each page, XeoPix stores:

  • rawSchemas — the JSON-LD blocks that parsed successfully, as structured data.
  • parseErrors — one entry per block that failed, holding the parser’s error message and the index of the block within the page.

For each finding, the audit issue’s details holds:

  • message — which block failed and what the parser said, for example Invalid JSON-LD syntax in script block 2: Unexpected token } in JSON at position 412.

The contents of a failing block are not stored verbatim. The block is in your page source at the index given, and copying arbitrary page markup into the audit record would mean retaining content the scan has no need for.

Examples

Trailing comma

<!-- Invalid: comma after the last property --> <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Organization", "name": "Acme Ltd", "url": "https://example.com", } </script>
<!-- Valid --> <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Organization", "name": "Acme Ltd", "url": "https://example.com" } </script>

Unescaped quote inside a value

<!-- Invalid: the inch mark closes the string early --> <script type="application/ld+json"> { "@type": "Product", "name": "27" Studio Display" } </script>
<!-- Valid --> <script type="application/ld+json"> { "@type": "Product", "name": "27\" Studio Display" } </script>

Unsubstituted template placeholder

<!-- Invalid: the templating engine never ran over this block --> <script type="application/ld+json"> { "@type": "Article", "headline": {{ post.title }} } </script>

Two top-level objects

<!-- Invalid: JSON allows exactly one top-level value --> <script type="application/ld+json"> { "@type": "Organization", "name": "Acme" } { "@type": "WebSite", "url": "https://example.com" } </script>
<!-- Valid: one graph containing both --> <script type="application/ld+json"> { "@context": "https://schema.org", "@graph": [ { "@type": "Organization", "name": "Acme" }, { "@type": "WebSite", "url": "https://example.com" } ] } </script>

Generating it safely

Serialising an object escapes quotes and backslashes for you, which removes the entire class of problem above.

const schema = { '@context': 'https://schema.org', '@type': 'Product', name: product.name, } // The replace() guards against a value containing a closing script tag. const json = JSON.stringify(schema).replace(/</g, '\\u003c')

References

Last updated on