JSON-LD @graph is the format Yoast SEO and Rank Math use to put every schema.org entity on a page into a single block. It is valid, Google reads it, and a surprising amount of software can't. We just found out our own AI readability and AEO checks were among that software. This post explains the mistake, shows how to avoid it in your own code, and lists what changes for your scores.

What is JSON-LD @graph?

There are two common ways to put several schema.org entities on one page. The first is one <script type="application/ld+json"> block per entity, each with its own @type:

{"@context": "https://schema.org", "@type": "Article", "headline": "..."}

The second, which Yoast, Rank Math and most WordPress SEO plugins use, is a single block holding every entity in a @graph array:

{
  "@context": "https://schema.org",
  "@graph": [
    {"@type": "Article", "@id": "https://example.com/post/#article", "author": {"@id": "https://example.com/#/person/1"}, "dateModified": "2026-09-01"},
    {"@type": "WebPage", "@id": "https://example.com/post/", "potentialAction": [{"@type": "ReadAction"}]},
    {"@type": "BreadcrumbList", "itemListElement": ["..."]},
    {"@type": "WebSite", "potentialAction": [{"@type": "SearchAction"}]},
    {"@type": "Person", "@id": "https://example.com/#/person/1", "name": "Jane Doe"}
  ]
}

Both forms are valid JSON-LD 1.1, and Google reads both. The @graph form is tidier: entities point at each other by @id instead of being nested and repeated, which is why the plugins prefer it. Yoast documents its graph as one connected set of pieces for exactly that reason.

The mistake: reading only the top-level @type

The @graph block has no top-level @type. The outer object is just a box. So any code that does this:

data = json.loads(script_tag.string)
if isinstance(data, dict) and "@type" in data:
    types.append(data["@type"])

finds nothing at all. The page has an Article, a breadcrumb trail, an author and a modification date, and the parser reports "no structured data". Checks that look for author or dateModified on the top-level object fail the same way, because on a Yoast page those fields live on the Article node inside the graph.

This bug is easy to write. Most tutorials use one entity per block. You only see the bug when you test on real WordPress pages.

How to parse JSON-LD correctly

Flatten every block into a list of entities before you inspect anything. Handle three shapes: a single object, an array of objects, and an object with @graph:

import json

def jsonld_entities(soup):
    entities = []

    def collect(node):
        if isinstance(node, list):
            for item in node:
                collect(item)
        elif isinstance(node, dict):
            if "@type" in node:
                entities.append(node)
            if "@graph" in node:
                collect(node["@graph"])

    for tag in soup.find_all("script", type="application/ld+json"):
        try:
            collect(json.loads(tag.string or ""))
        except (json.JSONDecodeError, TypeError):
            pass  # one broken block shouldn't hide the others
    return entities

Two more details catch people out:

  • @type can be a list. "@type": ["Article", "NewsArticle"] is valid. Normalise it to a list of strings before comparing, or a set lookup like t in {"Article"} raises TypeError: unhashable type: 'list'.
  • Plugins emit Action types. Yoast adds SearchAction, ReadAction and EntryPoint on every page. They are real schema.org types, so a validator that flags them as unknown will warn on every Yoast site there is.

What was wrong in SEO Score API

Our main SEO audit has always walked every JSON-LD node, so its structured data checks read @graph correctly. Our AI readability, AEO (answer engine optimization), SXO and AIO checks used a simpler reader that only looked at the top-level @type. On a Yoast or Rank Math site, one report could call the schema valid in one section and missing in another.

Here is what a WordPress site using @graph lost, and what it gets back after the fix:

Score What it missed Typical gain
AI readability 8-point structured data check, JSON-LD author and dateModified Up to 10 points
AEO Rich schema (Article, BreadcrumbList) About 16 points on a blog post
AEO FAQPage and HowTo credit, when present Up to 31 points in total
SXO JSON-LD breadcrumbs Breadcrumb check now passes
AIO JSON-LD authorship and dates Author and freshness checks now pass

Three smaller bugs we fixed at the same time

While fixing the parser we found three smaller bugs. All of them ship in the same release:

  • Reading ease on pages with inline markup. We extracted text without a separator, so <b>quick</b><i>brown</i> became one long word, which pushed Flesch scores down. In one test, a 50-word paragraph counted as a single word.
  • <script defer> counted as render-blocking. HTML parsers return an empty string for a bare boolean attribute, and an empty string is falsy. We now check whether the attribute is present, not what its value is.
  • Valid types flagged as unknown. HowToStep and the Yoast Action types above now pass the schema type check.

Will my score change?

It depends on how your schema is written and what is on the page:

  • Yoast, Rank Math or another @graph plugin: your AI readability and AEO scores will likely go up on your next audit.
  • Score monitors: you may see a one-time jump. That jump is the fix, not a change on your site.
  • Pages with bold, italic or linked text: reading ease may go up too.
  • Schema already in separate blocks: nothing changes for you.

Frequently Asked Questions

Does Google understand JSON-LD @graph?

Yes. Google's structured data parser reads @graph blocks, and standard Yoast and Rank Math output passes the Rich Results Test. The problem is limited to third-party tools that parse JSON-LD with a shortcut, reading only the outer object. If a tool says a WordPress page has no schema but Google's test finds it, the tool is the one at fault.

Should I switch away from @graph to fix this in other tools?

No. The @graph format is valid, widely used, and the cleanest way to describe connected entities such as an article, its author and its publisher. Rewriting your schema to suit a tool with a parser bug would make your markup worse. If a tool reports missing schema on a Yoast page, tell its authors and point them at this post.

How do I check what structured data my page actually has?

Run your URL through Google's Rich Results Test or the Schema Markup Validator. Both list every entity on the page, including those inside @graph. Then run an SEO Score API audit on the same URL: the structured data, AI readability and AEO sections should now agree with each other.