squirrelscan
GitHub

Invalid JSON-LD syntax: what breaks it and how to fix

A structured data block that will not parse, or parses but omits properties schema.org requires. squirrel names the exact type and property.

What JSON-LD is

JSON-LD is the format Google and other consumers read structured data in: a JSON document inside a <script type="application/ld+json"> tag, with a @context pointing at https://schema.org and a @type naming what the node describes. It sits beside the page content rather than inside it, so nothing about the rendered page changes when it is wrong.

Two different faults show up here. A syntax error means the block is not valid JSON at all, usually from a trailing comma, an unescaped quote in a value, or a template that emitted a raw newline inside a string. A validation error means the JSON parsed but a type is missing a property squirrel’s schema validator expects for it. The first makes the whole block unreadable; the second narrows what a consumer can do with it.

What squirrel checks

The rule reads the page’s parsed JSON-LD and emits exactly one check:

  • json-ld, at info status with the message No JSON-LD structured data found, when the page’s parsed JSON-LD is empty.
  • json-ld, at info status with the message JSON-LD detected but schema metadata unavailable, when a block was found but the parser could not produce validation metadata for it.
  • json-ld-valid, failing when any block failed to parse or the page’s JSON-LD has at least one validation issue. The message is Invalid JSON-LD syntax if there is a parse error and Schema.org validation errors detected otherwise. Items list the parse errors verbatim, then one entry per validation issue carrying the property path, labelled <type> missing <property> for a missing property and <type> has an invalid <property> for a value of the wrong shape.
  • json-ld-types, warning with the message JSON-LD present but no @type found, when the JSON-LD parsed cleanly but declares no type.
  • json-ld, passing with the message Valid JSON-LD with N type(s) and one item per type name.

A parse error anywhere in the page’s JSON-LD takes priority over a validation issue for the message, so a page with both reads Invalid JSON-LD syntax and still lists its validation issues as items. The validation runs at audit time, so a page reused from the crawl cache is checked by the current release, not by the one that first fetched it. The one exception is the untyped top-level nodes described below: a page an older release parsed did not keep them, so a reference to one resolves once the page is fetched again or the audit runs with --refresh. The severity is warning, and the check status is fail, so the finding appears at warning weight in the score.

What the validator accepts

The validator follows the schema.org range of each property it checks:

  • image (on Article, Product, LocalBusiness and Recipe) and Organization.logo take a URL, an ImageObject with a url or contentUrl, an {"@id": ...} reference, or an array of these. Anything else, such as a number, an object of another @type, or an ImageObject with no URL, is invalid.
  • A nested node with an @id, such as "publisher": {"@id": "https://example.com/#organization"}, is checked together with every other node that describes the same @id on the page: in the same @graph, another @graph, or another script block, including a top-level node that has an @id but no @type. A property given by any of them counts. This is how Yoast and @unhead/schema-org (Nuxt) link their nodes. A bare reference to an @id that no node on the page describes is checked as written, so publisher.name is still reported missing.
  • A node that carries both a @graph and its own @type, such as a WebPage wrapping an Organization, is checked as a node itself as well as through its children. A wrapper with no @type, which is what Yoast emits, contributes only its children.
  • In JSON-LD any property may hold one value or a list. Recipe.recipeIngredient takes a string or a list of strings, Recipe.recipeInstructions takes text, an object such as a HowToStep or HowToSection, or a list of either, and FAQPage.mainEntity and BreadcrumbList.itemListElement take one entry or a list.
  • Each Product.offers entry needs price, priceCurrency and availability. An AggregateOffer needs lowPrice and priceCurrency instead.
  • Article.publisher is optional, as in Google’s Article documentation. A publisher that is given needs a name and nothing else, so a Person publisher with no logo passes.
  • Each BreadcrumbList entry needs position, name and item, except the last crumb, which may leave out item as Google allows. The last crumb is the one with the highest position, or the one written last when no entry has a position. A crumb given as an {"@id"} reference takes its fields from the node it names, and a name given on the crumb’s item node counts.
  • Each FAQPage question needs name and acceptedAnswer.

How to fix it

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "Acme Coffee",
  "url": "https://example.com/"
}
</script>

Serialise the block from a real object with JSON.stringify rather than assembling it from a string template, which is where unescaped quotes and trailing commas come from.

The common false positive is a block injected by client-side JavaScript. squirrel reads the HTML the server sent unless browser rendering is on, so markup added after load is not there to parse and the page reports No JSON-LD structured data found.

Rule IDschema/json-ld-valid
CategoryStructured Data
ScopePer-page
Severitywarning
Weight5/10

Enable / disable

Disable this rule

[rules]
disable = ["schema/json-ld-valid"]

Disable all Structured Data rules

[rules]
disable = ["schema/*"]

Enable only this rule

[rules]
enable = ["schema/json-ld-valid"]

enable replaces the default ["*"], so this is the only rule selected. Leave disable out: a rule that matches any disable pattern is skipped even when enable names it. Only an enabled setting under a rule’s own [rule_options] table overrides both lists.

  • schema/article: the per-type check for articles, which names the specific properties it wants.
  • schema/product: the same for product pages, including offers and pricing.
  • schema/organization: the same for the site’s own identity markup.
  • content/broken-html: markup faults in the page body, which often share a root cause with a malformed script block.

Structured data findings ship in every audit next to the core SEO, performance and agent experience rules. See Fix SEO issues with AI for how an agent works through a report.

References

Check your site

Run squirrel audit https://example.com and open the Structured Data section of the report. Each failing page names the type and the property it is missing. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.

Type to search…

↑↓ navigate↵ openesc close