Schema markup work has three separate questions: is the JSON-LD valid, does the page meet a currently supported Google feature’s rules, and did Google actually show that feature? Validate all three independently. A SERP observation cannot tell you the page’s schema implementation, and valid markup does not guarantee display.
Start with currently supported result types
Google’s May 2026 update says FAQ rich results stopped appearing on May 7, 2026; the documentation was removed on June 15. Do not plan FAQ markup as a route to an FAQ search enhancement. Visible questions and answers can still help readers. Check the current supported-feature gallery before choosing another type.
Featured snippets, People Also Ask and other SERP features should not be treated as schema types you can enable by copying a competitor. Use Google’s structured-data policies and the specific feature requirements, with matching visible content.
Collect implementation evidence from HTML
Use the original HTML, or a rendered DOM when scripts inject the markup. Reader Markdown is useful for checking content readability but is not a guaranteed source of script tags or a schemas field. The parser below finds JSON-LD blocks in initial HTML and reports JSON validity without calling that result Google eligibility.
import json
from html.parser import HTMLParser
class JsonLdReader(HTMLParser):
def __init__(self):
super().__init__(convert_charrefs=False)
self.active = False
self.buffer = []
self.raw_blocks = []
def handle_starttag(self, tag, attrs):
if tag.lower()=="script" and dict(attrs).get("type", "").lower()=="application/ld+json":
self.active, self.buffer = True, []
def handle_data(self, value):
if self.active:
self.buffer.append(value)
def handle_endtag(self, tag):
if tag.lower()=="script" and self.active:
self.raw_blocks.append("".join(self.buffer))
self.active = False
def inspect_jsonld(raw_html):
parser = JsonLdReader()
parser.feed(raw_html)
rows = []
for index, raw in enumerate(parser.raw_blocks):
try:
value = json.loads(raw)
nodes = value if isinstance(value, list) else [value]
types = []
for node in nodes:
if isinstance(node, dict):
types.append(node.get("@type"))
types += [n.get("@type") for n in node.get("@graph", []) if isinstance(n, dict)]
rows.append({"block":index, "json_valid":True, "types":[t for t in types if t]})
except json.JSONDecodeError:
rows.append({"block":index, "json_valid":False, "types":[]})
return {"blocks":rows, "eligibility":"not_tested", "visible_content_match":"manual_check_required",
"limitation":"initial HTML only; render JavaScript separately when schema is injected"}
Build a verification record
| Evidence | What it establishes |
|---|---|
| Raw or rendered HTML | The actual JSON-LD present at the sampled time |
| Visible page content | Whether names, dates, offers and other statements agree |
| Vocabulary and required-property checks | Whether the selected supported feature is implemented correctly |
| Rich Results Test | Google’s test output for the exact sampled page |
| Search Console and repeat observations | Actual processing and observed appearances, with aggregation limits |
Test a synthetic page with one valid JSON-LD block and one malformed block. The parser should distinguish them and leave eligibility as not_tested. A page can pass that parser while violating feature requirements. A rich result can also be absent on a valid eligible page; do not turn either situation into a fabricated CTR gain.
Observe presentation without claiming causation
Use a Google SERP sample to record a query, market, timestamp and visible feature. Keep that observation separate from markup extraction. Track the appropriate GSC search appearance where available, and avoid summing different page and property aggregations. A before/after movement alone does not prove schema caused the change.
For implementation use the JSON-LD validation workflow. the rich-result monitoring workflow and the change ledger handle observations. This article owns eligibility and evidence selection; it does not replace those operational checks.