Structured data has a failure mode unlike almost anything else on a site: it breaks silently. A template change ships, the page looks identical, the tests pass, and three weeks later someone notices rich results have vanished from Search Console across ten thousand product pages. No error, no alert, no broken build, just markup that quietly stopped being valid.
The reason it's so easy to miss is exactly the reason it's so worth automating. Schema lives in templates, so when it breaks, it breaks everywhere at once, and nothing visible signals the failure. Manual spot-checking can't catch this reliably. Your build pipeline can.
CI/CD stands for Continuous Integration and Continuous Deployment, which is just the automated process that tests your code changes and ships them. This is a practical guide to building schema validation into that process: what to check, which tools do which job, and how to layer them so a broken template fails the build instead of reaching production.
Why manual validation doesn't scale
The standard advice is "test your markup in the Rich Results Test." That's fine for a single page during development. It falls apart as a quality-control strategy for three reasons.
Schema is template-driven. You don't have a hundred hand-written schema blocks; you have three templates producing a hundred pages. A bug in one template is a bug on every page it renders, and testing one URL tells you nothing about whether a different template broke.
Changes are invisible. A schema regression produces no visual change and no error message. Your build goes green. The page renders. Only the machine-readable layer is broken, and humans don't read that layer.
The failure is delayed and remote. By the time rich results drop out of Search Console, the change that caused it shipped weeks ago and is buried in history. The feedback loop is far too long to be a reliable safety net.
Automated validation inverts all three: it checks every template's output, it runs on every change, and it fails immediately, at the point the regression is introduced.
The layered approach
No single check catches everything, so effective schema quality control layers several, each catching what the previous one can't. Roughly from fastest and cheapest to slowest and most thorough:
- Syntax validation: is it readable JSON at all?
- Type checking: do the properties and types match schema.org?
- Required-property assertions: does each type have what its rich result needs?
- Schema.org compliance: is it valid against the vocabulary?
- Rendered-output crawl: is the markup actually in the served HTML?
- Post-deploy monitoring: did anything break in production?
The first four belong in the build, failing it when they find a problem. The fifth belongs in the build or in a staging check. The sixth is continuous monitoring. Let's take them in turn.
Layer 1: Syntax validation
The cheapest, fastest check, and it catches a surprising share of real failures.
JSON-LD is JSON, so it must be readable as JSON. The classic breakages are a template that drops in an unescaped string containing a quote, a trailing comma, or, the one that bites CMS-driven sites constantly, a </script> sequence inside user content that ends the block early.
In your pipeline, pull every JSON-LD block out of your rendered templates and run JSON.parse on each. A block that doesn't parse fails the build. This is a few lines of code and it catches the escaping bugs that account for a large share of "my schema stopped working" incidents.
If you build your JSON-LD as an object in application code rather than by gluing strings together, you largely avoid this whole class of bug, because JSON.stringify can only produce valid JSON. That's one of the strongest arguments for treating markup as data rather than text, a point we made in the Next.js schema guide.
Layer 2: Type checking with schema-dts
Google maintains schema-dts, a TypeScript library providing type definitions for the entire schema.org vocabulary. If you generate schema in TypeScript, this moves a whole category of errors from runtime to build time.
npm install --save-dev schema-dts
import type { Product, WithContext } from 'schema-dts'
const productSchema: WithContext<Product> = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: 'USD',
},
}Now a misspelled property, a wrong value type, or an invalid nesting is a build error, caught before anything runs. It won't tell you whether Google will show a rich result, that's a different question, but it eliminates the structural mistakes that are otherwise found only by a validator after deploy.
For stacks that don't use TypeScript, the equivalent is checking your generated objects against a JSON Schema description of the types you use. Less comfortable, same principle: catch structural errors mechanically rather than by eye.
Layer 3: Required-property assertions
Type checking confirms properties are valid. It can't confirm they're present. schema-dts is perfectly happy with a Product that has only a name, because most properties are optional in the vocabulary. But Google's rich result for Product needs offers with a price, and an Article needs headline, author, and a date.
This is where you write explicit tests encoding your own requirements:
import { describe, it, expect } from 'vitest'
import { buildProductSchema } from '../src/schema/product'
describe('Product schema', () => {
const schema = buildProductSchema(sampleProduct)
it('has a name', () => {
expect(schema.name).toBeTruthy()
})
it('has an offer with price and currency', () => {
expect(schema.offers).toBeDefined()
expect(schema.offers.price).toBeTruthy()
expect(schema.offers.priceCurrency).toBeTruthy()
})
it('has at least one image', () => {
expect(Array.isArray(schema.image) ? schema.image.length : schema.image).toBeTruthy()
})
})These tests encode your definition of complete for each type you use, informed by Google's required and recommended property lists. They're the layer that catches "someone refactored the product builder and the price stopped populating," which type checking sails right past, because a missing optional property is still perfectly valid.
Keep a reference of required-versus-recommended properties for each type you use, and turn each requirement into an assertion. Our schema types library is a useful starting point for what each type expects. This is unglamorous work, and it's the layer that catches the most consequential regressions.
Layer 4: Schema.org compliance
Beyond your own required-property rules, you want to know the markup is valid against the schema.org vocabulary generally: real types, real properties, correctly formatted values.
Since Google retired its old Structured Data Testing Tool, the general-purpose validation role is filled by the Schema Markup Validator at validator.schema.org, run by schema.org itself. It answers "is this valid schema.org?" independent of whether Google shows a rich result.
For automation, the practical options are checking against JSON Schema definitions inside your pipeline, or using a third-party validation service that wraps schema.org compliance checks and exposes it over an API. Judge any such service on whether it keeps up with what Google currently supports. A validator that still promises a Google rich result for HowTo or FAQPage is working from stale rules. Those types are still genuinely worth publishing, because AI systems and answer engines read them, but they no longer earn a Google rich result, and a validator that can't tell those two things apart will give you false confidence.
Layer 5: Crawl the rendered output
Every check so far validates the schema you intend to emit. This layer confirms the schema that's actually served, and the gap between those two is where JavaScript-heavy sites lose their markup entirely.
The failure mode: schema built inside a browser-side component validates perfectly in your unit tests, because the tests check the object you built. But a crawler fetching the page's initial HTML never sees it, because it only exists once JavaScript has run. Unit tests can't catch this. You have to check what the server actually sent.
In your pipeline, or against a staging deployment, fetch the built HTML for one representative page per template, pull the JSON-LD out of that raw response (the HTML your server sent, not what a browser shows after scripts have run), and assert it's present and correct. A quick way to sanity-check this on your own machine is loading the page with JavaScript turned off. If the markup vanishes, it's being added by the browser and is at risk.
For sites that legitimately build some content in the browser, test both the raw HTML and the finished page, and know which of your markup falls in which. The headless CMS guide goes deeper on where this bites.
Layer 6: Post-deploy monitoring
Even with everything above, production can surface issues your pipeline can't: how Google actually interprets your markup, errors found while crawling, and changes in what Google supports. Two tools cover this.
Search Console's Enhancement reports show how Google parsed your markup across the whole site after crawling, broken down by type. A spike in errors is your signal that a deploy broke something at scale. This is the definitive source, because it's Google's own reading of your live pages.
The Search Console URL Inspection API lets you check individual URLs automatically, including their rich result status, which makes it usable for scheduled monitoring of key pages rather than only manual spot-checks. It's limited in how often you can call it and works one URL at a time, so it suits watching a curated set of important templates rather than crawling everything. For catching regressions on your highest-value pages, though, it's a genuine automated signal from Google itself.
Set up a recurring job that inspects one representative URL per template and alerts you when rich result status changes. That closes the loop: your pipeline catches regressions before deploy, and this catches the ones that only show up once Google recrawls.
A realistic pipeline
Putting the layers together, a pragmatic setup for a mid-sized site:
On every pull request:
- Type-check schema generation with
schema-dts(it rides along with your normal type-check run, so it costs nothing extra) - Run required-property unit tests for each schema type
- Build the site and extract JSON-LD from representative rendered pages, parse each block, and assert it's there
On merge to main, before deploy:
- Crawl the staging build and validate the rendered output for each template
- Optionally run a schema.org compliance check for the types you care about
Continuously:
- Monitor Search Console Enhancement reports
- Run scheduled URL Inspection API checks on key templates, alerting on rich result status changes
Most of this is cheap. The type check rides along with compilation. The unit tests are ordinary tests. The build-time extraction is a small script. Only the rendered crawl and the monitoring take real setup, and they're the ones that catch the failures the others can't.
A worked GitHub Actions example
To make the pipeline concrete, here's the shape of a workflow that blocks a build on schema checks. The specifics vary by stack, but the structure generalises.
name: Schema QA
on: [pull_request]
jobs:
schema:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
# Layer 2: type-check schema generation (rides along with tsc)
- run: npm run typecheck
# Layer 3: required-property unit tests
- run: npm run test:schema
# Build the site so we can inspect real output
- run: npm run build
# Layer 1 + 5: extract JSON-LD from built pages, parse, assert presence
- run: node scripts/validate-schema.mjsAnd the essence of that extraction script: read the representative built pages, pull out every JSON-LD block, and fail on any that doesn't parse or is missing.
// scripts/validate-schema.mjs
import { readFile } from 'node:fs/promises'
const pages = [
{ path: 'dist/products/sample/index.html', expect: 'Product' },
{ path: 'dist/blog/sample/index.html', expect: 'BlogPosting' },
{ path: 'dist/index.html', expect: 'Organization' },
]
let failed = false
for (const page of pages) {
const html = await readFile(page.path, 'utf8')
const blocks = [...html.matchAll(
/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g
)].map((m) => m[1])
if (blocks.length === 0) {
console.error(`FAIL ${page.path}: no JSON-LD found`)
failed = true
continue
}
const types = new Set()
for (const block of blocks) {
try {
const data = JSON.parse(block)
const nodes = data['@graph'] ?? [data]
for (const n of nodes) if (n['@type']) types.add(n['@type'])
} catch (e) {
console.error(`FAIL ${page.path}: invalid JSON-LD (${e.message})`)
failed = true
}
}
if (!types.has(page.expect)) {
console.error(`FAIL ${page.path}: expected ${page.expect}, found ${[...types]}`)
failed = true
}
}
process.exit(failed ? 1 : 0)That single script covers three of the six layers at once. It confirms the markup is present in the built HTML (layer 5), that every block parses (layer 1), and that each page carries the type it should. It's around forty lines, it runs in seconds, and it would have caught most of the "schema silently disappeared" incidents that motivate this whole post.
Extend it as needed: assert required properties inline, check that no page emits a type you've stopped supporting, or compare the extracted schema against a saved copy so any change shows up in review. Even the minimal version above is dramatically better than nothing, and nothing is what most sites have.
What to actually assert
A common mistake is asserting too much or too little. Some guidance on calibration:
Assert presence and structure, not exact values. Test that offers.price exists and isn't empty, not that it equals "129.00". The second version breaks every time a price changes, which is noise, not signal.
Assert per type, from Google's requirements. Encode the required properties for each rich result you rely on. Don't assert properties Google treats as optional unless you specifically want them.
Assert the type still does what you think. Given how often Google changes which types earn a rich result, it's genuinely useful to fail the build when a template emits a type you're counting on for a rich result that Google no longer shows. Our rundown of which schema types still earn rich results is the list to check your assumptions against. Nobody wants to discover months later in Search Console that they've been waiting on a result that was never coming.
Don't assert Google will show a rich result. You can't. Being eligible isn't the same as being entitled, and Google decides what to display. Assert eligibility (valid, complete, supported markup), not the outcome.
Match markup to visible content in code review, not in the pipeline. The one requirement a pipeline struggles to check by itself is whether your markup matches what's actually visible on the page. That's a judgment about meaning, and a script can't make it. Keep it in code review rather than pretending otherwise.
The distinction that underlies all of this
Two questions run through everything above, and confusing them is the most common conceptual error:
"Is my markup valid?" is a mechanical question, answerable in your pipeline. Valid JSON, real schema.org, required properties present.
"Will Google show a rich result?" is a Google question, answerable only by Google, and never guaranteed. It depends on your markup being valid and on Google choosing to display it.
Your pipeline can fully automate the first, and for the second it can confirm you've met the requirements. It cannot promise the outcome. Building around the answerable question, while monitoring production for the rest, is the right division of labour.
One fact is worth keeping in view through all of it: structured data isn't a ranking factor, so this pipeline protects rich result eligibility and how well machines understand your pages, not rankings directly. As more discovery happens through AI assistants and answer engines that read your markup to decide what your page is about, that legibility is worth protecting on its own terms. It's just worth being precise about what you're protecting.
Frequently Asked Questions
AI Schema Gen generates and validates structured data as part of producing it, across 827+ schema.org types, so validity is built in rather than bolted on afterwards. Start free, read the setup documentation, or browse more schema guides.
Generate perfect schema in 30 seconds
AI Schema Gen handles everything automatically, free to start.
Get Started Free