Schema engineering

Building a Schema.org @graph That Validates on the First Try

By Joseph W. Anady · Published 2026-05-21 · Updated 2026-05-22

Most agencies hand you a Schema.org JSON-LD block that fails the Rich Results Test on the first try. Duplicate @id values, orphaned references, mismatched types, missing required fields. The pattern that actually validates is a single @graph block with explicit @id threading, and it is simpler than the typical multi-block approach.

Short answer: Use a single JSON-LD script tag with "@graph" as the top-level key. Give every node an explicit @id that resolves to a unique fragment on your site. Cross-reference nodes by @id using the {"@id": "..."} shortcut. Validate with Google's Rich Results Test before deploying.

Why multiple JSON-LD blocks fall apart

Search engines do not collate multiple JSON-LD blocks the way you might expect. Each block is parsed independently. When two blocks both define an Organization with the same canonical URL but different name values, Google does not merge them; it picks one and discards the other. Worse, the choice is not deterministic across crawls. Inconsistent rich results follow.

The fix is structural: one block, one graph, every entity addressable by @id.

The shape that always validates

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": ["Organization", "ProfessionalService"],
      "@id": "https://example.com/#organization",
      "name": "Example Studio",
      "url": "https://example.com/",
      "logo": "https://example.com/logo.svg",
      "founder": { "@id": "https://example.com/#founder" },
      "sameAs": [
        "https://www.linkedin.com/company/example-studio",
        "https://x.com/examplestudio"
      ]
    },
    {
      "@type": "Person",
      "@id": "https://example.com/#founder",
      "name": "Jane Doe",
      "worksFor": { "@id": "https://example.com/#organization" }
    },
    {
      "@type": "WebSite",
      "@id": "https://example.com/#website",
      "url": "https://example.com/",
      "publisher": { "@id": "https://example.com/#organization" }
    }
  ]
}

Three nodes (Organization, Person, WebSite), three @id values, two cross-references. Google parses this as a single connected graph and surfaces all three entities in the Knowledge Graph and rich result candidates.

Common mistakes that break the graph

Duplicate @id collisions

If two nodes in the graph share the same @id, Google merges them and may keep only one. If you have Person nodes for multiple authors, each needs its own @id.

// BAD
{ "@type": "Person", "@id": "https://example.com/#author", "name": "Jane" }
{ "@type": "Person", "@id": "https://example.com/#author", "name": "John" }

// GOOD
{ "@type": "Person", "@id": "https://example.com/#jane", "name": "Jane" }
{ "@type": "Person", "@id": "https://example.com/#john", "name": "John" }

Orphaned cross-references

If you reference an @id that does not exist in the graph, the cross-reference silently drops. This is a common pattern when copy-pasting Person nodes between sites: the @id still points at the old site.

Missing required fields

Schema.org's Rich Results documentation lists the fields each type needs to qualify for rich results. LocalBusiness requires address, telephone, and either geo or a Place reference. Organization needs name and url at minimum. FAQPage needs at least two Question nodes with Answer text. Validate with the Rich Results Test before deploying.

Per-page extension pattern

The graph above lives in the site shell (every page). On individual pages, add a second JSON-LD block that extends the graph with page-specific entities: a BlogPosting, a Service description, a BreadcrumbList. Cross-reference back to the shell's Organization and Person nodes via @id.

// On a blog post page, in addition to the shell graph:
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "@id": "https://example.com/blog/post-slug/#post",
  "headline": "Post title",
  "author": { "@id": "https://example.com/#founder" },
  "publisher": { "@id": "https://example.com/#organization" },
  "datePublished": "2026-05-22T08:00:00-05:00"
}

The BlogPosting does not redefine the author or publisher; it points at the existing graph nodes by @id. Google resolves the references during indexing and the BlogPosting inherits the validated identity of the Organization.

Validation workflow

  1. Paste the rendered HTML into Google's Rich Results Test
  2. Fix any field-level errors first (red items)
  3. Then fix warnings (yellow items)
  4. Confirm the test shows the eligible rich result types you targeted
  5. Re-test after every JSON-LD change in production

What this earns you

A clean @graph that validates on the first try is the foundation of every rich result Google ranks for: Organization Knowledge Panels, Person Knowledge Panels, FAQ rich snippets, Breadcrumb rich results, LocalBusiness map cards. Without it, your site competes on title tag and meta description alone. With it, you compete on the structured-data surface area that Google's index actually understands.

If you want this done correctly the first time, the studio runs schema markup engineering as a standalone service. Or read the research thread on schema graph completeness.