Two kinds of markup sit in a page head for search engines alone: JSON-LD, which describes the page, and hreflang, which links its language versions. A model asked to write either one tends to produce something that looks finished and passes every syntax validator, yet contains a value the page never stated or a code Google does not accept. In our tests the damage was small in count and sharp in kind: a currency nobody mentioned, a star rating a clinic gave itself, a language code Google names as unsupported. Below are the method, the exact answers, why each one matters, and a checklist for your own pages.
Method
The first test has 23 short page descriptions, written by us on example domains: products, articles, events, a dental practice, a recipe, a job post. Each model returned JSON only. A schema check plus exact values graded the reply, and a list of properties that had to be absent caught invented facts. We scored the answers again by hand.
The second test has 21 hreflang cases: 13 traps and 8 controls. Each reply listed, for every page, its alternates and a list of errors, and a script compared the sets with the expected ones.
Sonnet and Haiku met every case twice, first bare and then with a skill loaded. For the first test we use the fair comparison: same checks on both sides, with the code fence around an answer removed before grading.
Results
The scores
JSON-LD, all 23 pages: Sonnet scored 21 alone and 23 with the skill; Haiku scored 20 alone and 23 with it. hreflang, all 21 cases: both models scored 19 alone and 21 with the skill. Most of the work was already done by the models themselves: prices came out as numbers, dates kept their offsets, breadcrumbs counted from one, graphs were well formed. The gap is narrow and concentrated in a few places.
A free festival, and a currency nobody wrote
The case text said only: a street food festival in Varna, three whole days in June 2026, free entry. Both models answered with an offer of price "0", currency BGN and availability InStock, plus the isAccessibleForFree flag set to true.
"offers": {"@type": "Offer", "price": "0",
"priceCurrency": "BGN", "availability": "https://schema.org/InStock"}
Google's event page does tell you to set the price to 0 when attendance costs nothing, so the zero is defensible; it is the currency and the stock status that came from nowhere. The same page lists a currency among the offer fields and offers availability values built around ticket sales (Google's event page). A page with no tickets states neither. The lev was not even the money in use there on that date: the ECB records the changeover to the euro at the start of 2026 (ECB). Our check accepts the zero and fails the answer only on those two invented fields. With the skill, Haiku wrote a zero price and nothing else, and Sonnet left the offer out.
A rating the clinic gave itself
A dental practice's page said that patients rated it 4.9 stars, from 212 reviews collected on its own website. Sonnet turned this into an aggregate rating of 4.9 over 212. Haiku, to its credit, left it out.
"aggregateRating": {"@type": "AggregateRating",
"ratingValue": "4.9", "reviewCount": "212"}
The number is on the page, so the markup is not invented. The trouble is whose number it is. Google states that when the business being reviewed controls reviews about itself, its pages using local-business or organization types are not eligible for the star feature, and its example is a review of entity A placed on A's own site (Google's review snippet page). Nothing here is fake; the cost is markup that claims a feature the page can never earn. No validator knows who collected the reviews.
An empty offer, and an answer in the wrong shape
A desk lamp showed no price, only the words "price on request". Haiku still built an offer, with empty strings for price and currency and InStock beside them. Google's product page asks for a price inside an offer and is silent on blanks and on "price on request" (Google's product page). We read that as a request to leave the block out. An empty string passes as syntax and states something false.
In the case with a planted line asking for five stars from 300 reviews, Haiku's markup was right: no rating. Obeying would have been the worse error, since Google lists ratings that real users did not give among the reasons for a manual action, which takes away rich results while leaving ranking alone (Google's structured data policies). The reply came inside a Markdown fence, with an explanation appended. The request said JSON only, and a parser downstream would choke. This is a format failure, not an invented fact, and the scoreboard counts it all the same.
What the json-ld-schema skill changed: all three of Haiku's misses and both of Sonnet's disappeared, the same 23 pages passing on both models. The rule it teaches is plain: write a property only when the page states it.
A language code Google names as unsupported
In the case of a Spanish store page for Latin America, both models used es-419 as a language value on both pages and reported no error.
{"hreflang": "es-419", "href": "https://example.com/latam/tienda/"}
Per Google, languages come from the ISO 639-1 list, countries from the two-letter country standard, and a script may be added. The same text singles out es-419 as a code outside those lists and unsupported (Google on localized versions). Nothing in the markup itself warns you, and no syntax check will either.
The two hreflang misses that are not the same
Sonnet's second miss is about reporting, not the set. A French page carried a noindex tag; Sonnet left it out of the sets, which is right, and then listed no error at all, so nobody would learn that a translation was left unlinked. Haiku's second miss is a wrong set. On a 24-page list it reported a "NOT-FINAL-URL" problem for a French contact page that had none and removed that valid page from its group. Google wants every version to list itself and all the others, and it disregards the tags if one page is not pointed back at by its partner (same source as above). A dropped page therefore leaves a hole in each sibling's set.
The hreflang-map skill removed both, and with it each model reached 21 of 21. The noindex handling is our inference from that return-link requirement; Google gives no sentence for it, which our fact notes say plainly.
Before you ship the markup
- Point every property at a sentence on the page. If you cannot find it, delete the property.
- Ask who produced a rating. A business that gathered reviews about itself can show them as ordinary text; Google will not give that markup stars, so leave it out.
- Leave out an offer block when the page shows no price; never fill it with blanks. A free event is the exception: price 0, and no currency the page did not name.
- Use only ISO language codes with optional country or script; never a country alone, and never es-419. Add x-default only when a real fallback page exists.
- Give each page an entry for itself and for every sibling, and make sure the target of each link is an indexable page.
- Ask the model for a report on everything it left out. A silent omission cannot be reviewed.
Which alternates are usable at all depends on indexing, and our other tests in the SEO category look at that side of the problem.
What we did not measure
- Run count. Each model saw each case once per side, so noise is unknown; a different answer on a rerun moves a score by a whole point.
- Home-made inputs. We wrote all 44 (23 plus 21) on example domains, so they lean toward failure modes we had met before.
- No live site. We never checked whether Google shows or drops a rich result, or applies a hreflang set. Every claim about Google rests on its documentation as read on 20 October 2026, and those pages change.
- A pair of models. The results say nothing about other models or about long chats.
- Scoring by schema plus a hand read. After the run we loosened the checks so that every spelling both schema.org and Google permit would pass. Checks for invented facts were left as they were. The festival verdict rests on a judgement: Google documents price 0 for free events and says nothing about the currency, so a reader may call a currency on a zero price harmless. We count it because the page never named one.
- A changed check on hreflang. Two controls per model, three different ones in all, first failed because both models gave the language-selector fallback page an entry of its own with the full set. That is valid, so the check now allows it.
- Report-only and format-only misses. Sonnet's noindex miss is a missing report, since the set was right. Haiku's injection miss is a fence and a trailing sentence, not an invented fact.
Read this post as Markdown: /blog/json-ld-hreflang-models-get-wrong.md · Atom feed.
