JSON-LD Review Generator: aggregateRating for AI Citation
Reviews and aggregateRating in JSON-LD: generator patterns for 3 verticals, AI engine weighting, and 9 silent failure modes.
Here is the situation. Your bakery’s site has, in its <head>, a JSON-LD block that declares aggregateRating: 4.7 over reviewCount: 312. Your Google Business Profile, on the same Tuesday morning, displays 4.6 (287). You ask ChatGPT, Claude, Perplexity, and Gemini about your shop. One quotes 4.7. One quotes 4.6. One says “around 4.6 to 4.7.” One quotes neither and just says “well reviewed.” Four engines, one bakery, four different answers about the same rating.
That divergence is not noise. It is the structured-reputation layer being read by four different resolvers that each have a different opinion about whose count to trust. This article is the engineer’s view of how that layer is wired, what Review and aggregateRating actually mean once you stop treating them as a star-bar widget and start treating them as a typed graph, and why the most common mistake in Review schema (the one I find on six out of ten local-business sites I open the source on) is not getting the syntax wrong but getting the graph shape wrong in a way the parser silently downgrades.
One disclosure before the JSON, because the honesty of the rest depends on it. I am reasoning from the schema.org type definitions, Google’s structured-data documentation, public Knowledge Graph behavior, and what I have been able to verify by probing the four major assistants with controlled prompts. I am not claiming to have read any engine’s internal scoring code. Read this as documented architecture-based inference plus light probing, not as a leaked spec.
The nesting, in one block
Most of the confusion about Review schema dissolves once you draw the graph instead of the form. The relevant types nest like this on a LocalBusiness (or Restaurant, or Bakery, or any other subtype):
{
"@context": "https://schema.org",
"@type": "Bakery",
"name": "Corner Crumb",
"@id": "https://cornercrumb.example/#bakery",
"address": { "@type": "PostalAddress", "streetAddress": "12 Mill St", "addressLocality": "Brooklyn" },
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.7",
"reviewCount": "312",
"bestRating": "5",
"worstRating": "1"
},
"review": [
{
"@type": "Review",
"author": { "@type": "Person", "name": "Mei T." },
"datePublished": "2026-06-12",
"reviewRating": { "@type": "Rating", "ratingValue": "5", "bestRating": "5" },
"reviewBody": "The morning bun is the reason I changed my walking route."
},
{
"@type": "Review",
"author": { "@type": "Person", "name": "Jordan K." },
"datePublished": "2026-06-08",
"reviewRating": { "@type": "Rating", "ratingValue": "4", "bestRating": "5" },
"reviewBody": "Croissants are top tier. Coffee was lukewarm twice in a row."
}
]
}
A few things to notice that the documentation does not put in one place. aggregateRating is a property of the LocalBusiness node, not a node sitting next to it. review (singular property, plural array value) is also a property of the same parent. Each Review is its own typed node with its own reviewRating, and that inner rating is a Rating type, not an AggregateRating. The parser cares about that distinction even though humans flatten the two in their heads. bestRating and worstRating are optional but make the rating interpretable on an absolute scale. Without them, a ratingValue of 4.7 is meaningless to a parser that does not know whether your maximum is 5 or 10.
The schema is a small graph. The mistake people make is treating it as flat fields on a form, and a flat-field mental model is what produces the most common failure mode in this layer, which I will get to in a moment.
Generator patterns for three common verticals
The nesting shown above is the canonical shape. The templates below are the same nesting adapted to three verticals that reach for slightly different subtypes and slightly different Review content. Copy the one closest to your business and edit the fields in place; the graph shape is what matters, not the prose in any single reviewBody.
Restaurant
{
"@context": "https://schema.org",
"@type": "Restaurant",
"name": "Kitasando Table",
"@id": "https://kitasando-table.example/#restaurant",
"servesCuisine": "Modern Japanese",
"priceRange": "$$",
"address": { "@type": "PostalAddress", "streetAddress": "3-1 Kitasando", "addressLocality": "Shibuya" },
"sameAs": [
"https://www.google.com/maps/place/?q=place_id:ChIJXXXXXXXX",
"https://tabelog.com/tokyo/A1306/A130603/13000000/"
],
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.6",
"reviewCount": "184",
"bestRating": "5",
"worstRating": "1"
},
"review": [
{ "@type": "Review", "author": { "@type": "Person", "name": "Aya S." }, "datePublished": "2026-08-02", "reviewRating": { "@type": "Rating", "ratingValue": "5", "bestRating": "5" }, "reviewBody": "The eight-course tasting was paced like a good album — each dish followed the last." },
{ "@type": "Review", "author": { "@type": "Person", "name": "Devon L." }, "datePublished": "2026-07-24", "reviewRating": { "@type": "Rating", "ratingValue": "4", "bestRating": "5" }, "reviewBody": "Excellent food, service warm; the room ran a little loud on Saturday." }
]
}
Cafe
{
"@context": "https://schema.org",
"@type": "CafeOrCoffeeShop",
"name": "Slow Ferment",
"@id": "https://slowferment.example/#cafe",
"servesCuisine": "Specialty Coffee",
"address": { "@type": "PostalAddress", "streetAddress": "220 Mill Ave", "addressLocality": "Portland" },
"sameAs": ["https://www.google.com/maps/place/?q=place_id:YYYYYYYY"],
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.8",
"reviewCount": "97",
"bestRating": "5",
"worstRating": "1"
},
"review": [
{ "@type": "Review", "author": { "@type": "Person", "name": "Rae M." }, "datePublished": "2026-08-05", "reviewRating": { "@type": "Rating", "ratingValue": "5", "bestRating": "5" }, "reviewBody": "The natural-process Ethiopian is worth the walk from the train." },
{ "@type": "Review", "author": { "@type": "Person", "name": "Sam O." }, "datePublished": "2026-07-30", "reviewRating": { "@type": "Rating", "ratingValue": "4", "bestRating": "5" }, "reviewBody": "Great coffee, seating fills up before ten on weekends." }
]
}
Retail store
{
"@context": "https://schema.org",
"@type": "Store",
"name": "Foxglove Books",
"@id": "https://foxglovebooks.example/#store",
"address": { "@type": "PostalAddress", "streetAddress": "77 Elm St", "addressLocality": "Somerville" },
"sameAs": ["https://www.google.com/maps/place/?q=place_id:ZZZZZZZZ"],
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.9",
"reviewCount": "213",
"bestRating": "5",
"worstRating": "1"
},
"review": [
{ "@type": "Review", "author": { "@type": "Person", "name": "Priya R." }, "datePublished": "2026-08-01", "reviewRating": { "@type": "Rating", "ratingValue": "5", "bestRating": "5" }, "reviewBody": "The staff picks shelf is how I've found my last four favorite novels." },
{ "@type": "Review", "author": { "@type": "Person", "name": "Ethan Q." }, "datePublished": "2026-07-19", "reviewRating": { "@type": "Rating", "ratingValue": "5", "bestRating": "5" }, "reviewBody": "Small, thoughtful, and the used-fiction pricing is honest." }
]
}
Three things travel through every one of these templates: the parent node carries a stable @id, sameAs reaches out to at least one canonical external mirror (Google Maps, Tabelog, whichever platform hosts the reputation you are claiming), and each Review has a real datePublished from inside the last quarter. Those are the three fields that separate a template that only validates from a template that gets read as a corroborated reputation graph.
Why aggregateRating alone is invisible to AI
Here is the failure mode that, more than any syntax error, costs local businesses citation: declaring aggregateRating with no review array beneath it. The schema validates. The Rich Results test goes green. And the assistant quietly trusts it less than the markup that came with two real Review children.
The mechanical reason is provenance, and it is the same reason I traced in the prose-versus-schema piece on this site about which encoding an assistant believes when the structured data and the page prose disagree about the same business. An aggregateRating is a summary statistic. A statistic with no underlying observations attached is, from the parser’s point of view, an unverifiable claim. Anyone can write 4.9 (1,247) into a JSON block. When the parent node also carries an array of Review children with authors, dates, and individual ratings whose mean roughly matches the aggregate, the aggregate becomes a checkable statistic. The model is not running arithmetic on every page, but the presence of the underlying graph is what bumps the assertion from “claimed” to “corroborated by the same document.” That is exactly the kind of internal consistency signal a retrieval pipeline can score cheaply and weight heavily.
You can see this same pattern in how Google’s own Rich Results guidance treats Review markup: the structured-data quality flags that the LLMO Framework now folds into its Discoverability component all light up the same way. Present-but-unbacked statistics are deprioritized relative to present-and-backed ones, across both human-search SERPs and AI answer surfaces. In the standardizing LLMO vocabulary, Review schema is being treated as the reference implementation for the reputation citation layer, the place where the framework’s three components (Discoverability, Retrievability, Citation) all land on one entity at once, because reputation is the rare attribute that has to be discoverable as structure, retrievable as individual observations, and citable as a summary, all from the same node.
First-party schema vs Google Business Profile — the provenance conflict
Now back to the four assistants quoting four different numbers. The mechanics underneath that:
| Source | Where the rating lives | Authority weight | When it gets quoted |
|---|---|---|---|
| First-party JSON-LD on your site | aggregateRating on LocalBusiness node | Medium — internally consistent if review children present, otherwise low | When the assistant is reading your own URL or its mirror |
| Google Business Profile | Knowledge Graph attribute on the resolved entity | High — corroborated by Google’s own review intake | Default for “what’s [business name] rated” queries |
| Third-party platforms (Yelp, TripAdvisor, Tabelog) | Their own structured data, surfaced via their own schema | Medium-high — corroborates or contradicts the KG | When the assistant is aggregating across sources |
sameAs linkages between the above | Glue, not data | Multiplier on the others | When the assistant is doing entity reconciliation |
The honest answer to “whose number wins” is: it depends on which surface the assistant is grounding from at the moment of the query, but Google Business Profile tends to be the anchor against which everything else is judged. If your first-party JSON-LD says 312 reviews and your GBP says 287, the assistant is not going to do a third-party audit to decide who is right — it is going to treat the GBP figure as canonical and your schema as a possibly-stale mirror. The reverse case (your site says fewer, GBP says more) is read the same way: GBP wins.
This is the same provenance hierarchy I worked through in the three provenance paths for AI assistants, applied specifically to the reputation surface: first-party schema, Knowledge Graph, third-party republish. The shape of the rule is identical: corroborated beats uncorroborated, and the Knowledge Graph is the corroboration point. Where Review schema differs from, say, opening hours is that the disagreement is almost always a count mismatch, not a value mismatch, because everyone is measuring the same underlying reviews. They just refresh on different cadences. Which means the cure is not “argue with Google” but “stop letting your reviewCount drift more than a week behind the GBP figure.”
How the four engines weight aggregateRating against individual review recency
The provenance conflict above is about whose count wins. There is a second axis underneath it: even inside a single source, engines split on whether the cumulative aggregateRating or the recency of the top few Review children carries more weight when the answer is generated. What I observe when I probe the same business across the four surfaces with the same question (“Is this place still worth going to?”) looks roughly like this — inference from published grounding behavior plus probing, not a leaked spec:
- ChatGPT (browsing):
aggregateRatingprimary as the headline number, but the browsing pass will fetch and quote the two or three most recentreviewBodytexts when the query has a recency lean.datePublishedclearly influences which reviews get lifted; ratings older than a year are rarely the ones quoted verbatim. - Perplexity: shows the aggregate in the business card at the top of the answer, then surfaces individual reviews as source cards below it. Recent reviews are visually up-ranked; the card layout puts freshness in the frame even when the aggregate is the headline number.
- Claude: leans on
aggregateRatingwhen the question is comparative (“is it well-rated?”) and shifts weight onto the most recentReviewchildren when the question is diagnostic (“has quality slipped lately?”). The pivot is the question shape, not a fixed policy. - Gemini: closest to Google Search behavior — the aggregate anchors the answer, and review snippets are typically lifted from Google Business Profile rather than from the site’s own
Reviewchildren, unless the two surfaces aresameAs-linked and internally consistent.
Two takeaways drop out of this. First, the aggregate is doing the top-line work but the recent Reviews are doing the quotable work, so a site that ships aggregateRating without recent Review children is silently sacrificing the second surface. Second, datePublished is not a decorative field; it is the axis engines sort on when they pick which review to lift. That is the same behavior pattern I traced under state fields for local businesses — recency-typed data is what the engines actually reach for when the query has a time bias.
The other place this weighting shows up is the split between brand-named queries (“Slow Ferment reviews”) and category queries (“best specialty coffee in Portland”). Category queries are the ones where aggregateRating acts as a ranking filter across a candidate pool; brand-named queries are the ones where the individual Reviews get quoted. I unpacked that whole axis in direct brand query vs category query, and Review markup is one of the fields where the split matters most.
What “consistent” actually means for Review schema
You do not have to mirror Google exactly. You have to be internally consistent in a way the parser can verify, and externally reconciled in a way the assistant can link. The four things that move the needle, in priority order:
aggregateRatingaccompanied by at least 3–5 realReviewchildren, with authors, dates, and individualreviewRatingvalues whose mean is within ~0.2 of the aggregate. This is the present-and-backed signal.bestRatingandworstRatingdeclared explicitly, so the parser does not have to assume your scale.sameAslinkages from yourLocalBusinessnode to your GBP URL and any platforms whose reviews you are citing. Without these, the assistant is doing string-match entity reconciliation, which is the exact failure mode I unpacked in NAP consistency at the entity reconciliation level.datePublishedon everyReview, recent. Areviewarray where the most recent entry is from 2023 reads as a fossilized testimonial page and depresses the trust signal even when the aggregate is high.
What does not move the needle: writing thirty Review children when you have actually had three customers, gaming ratingValue to “5.0” when your GBP shows 4.3, or padding reviewBody with keyword-stuffed prose. The parser does not care about the prose; the assistant does, but only as a quotation source. A quote that contradicts the aggregate is worse than no quote.
Nine failure modes in Review and aggregateRating markup
Nine ways this graph goes wrong in the wild, in roughly the order I run into them when I read local-business source. Each of these downgrades the reputation signal without breaking validation, which is what makes them expensive — the Rich Results test says green and the assistant still under-cites.
aggregateRatingpresent,reviewarray missing. Covered above; the highest-frequency failure. The aggregate becomes a claimed statistic with nothing to corroborate it internally.reviewCountset to0. Ships when the template renders before any review has been ingested. Parsers read this as an explicit assertion of “zero reviews,” not “no data yet,” and mute the reputation signal accordingly.ratingValuepopulated butbestRatingandworstRatingomitted. The parser is left to assume a 5-scale. When that assumption is wrong — a 10-scale, or a 100-point rating imported from an external dataset — the number is silently misread.reviewpresent but its innerreviewRatingtyped asAggregateRatinginstead ofRating. A copy-paste error that validates but confuses parsers; the inner node is an observation, not a summary, and the wrong type collapses that distinction.reviewchildren ship withoutdatePublished. The recency axis engines sort on collapses; the entire array reads as undated testimonial content, which is exactly the content type Gemini and ChatGPT are least likely to quote.reviewchildren ship with anonymous or generic authors — “A customer,” “Guest,” or the site’s brand name in theauthor.namefield. Reviews without aPerson-typed author with a realnameare the first ones dropped when the assistant picks which one to lift.reviewBodyunder ten words, or literal star-only content (”★★★★★”). The assistant has nothing to quote. The review still counts towardreviewCountbut contributes zero to the citable surface.itemReviewedset on individual Reviews but no@idlink back to the parentLocalBusinessnode. Each Review floats as an orphan entity; the reconciliation pass cannot fold them under the business. This is the reputation-side companion to the entity-wiring pattern in knowledge graph entity linking.aggregateRating.ratingValuedrifts more than 0.2 away from the mean of the shippedReview.reviewRatingvalues. Internal inconsistency. The site is now claiming a summary that its own observations contradict, which is the exact configuration a corroboration check catches. This is a strictly worse failure mode than mode #1, because a wrong aggregate over real reviews is louder than a lonely aggregate over no reviews.
The remediation for the first eight is straightforward — populate the missing field or re-type it. The ninth is the one that needs discipline: the Review array on the page has to be a sample of the underlying corpus, not a curated highlight reel of five-star quotes, because the moment it stops being representative the aggregate stops being corroborated by it.
A small deflation
I would like to write that getting Review schema right guarantees citation. It does not. I have probed sites with textbook-clean Review and aggregateRating graphs where the assistant still quotes the GBP number and ignores the first-party markup entirely, because the engine’s grounding pass landed on GBP first and never bothered to fetch the site. Schema correctness is a necessary condition for being citable on this surface, not a sufficient one. The LLMO Framework’s Citation component is explicit that all three of Discoverability, Retrievability, and Citation have to land before reputation gets carried into an answer, and the schema is only handling the first one. Get the structure right and you have earned the right to be read; whether you get quoted still depends on which surface the assistant picked up first.
One step you can do today
Open your own site’s source. Search for aggregateRating. If you find it, look immediately above and below for a review array on the same parent node. If there is no array — if the aggregate is sitting there alone, with no individual Review children attached — that is the single highest-leverage fix in this entire layer. Add three to five real Review nodes underneath it, with authors, dates, and individual ratings that roughly average to the aggregate, and validate that the parent LocalBusiness node carries a sameAs to your GBP URL. That is the move that turns a claimed rating into a corroborated one, and corroborated is the encoding an assistant will repeat.
Reputation is the rare attribute where we already write our customers’ words down for them. The structured layer is just the place where we let a parser confirm we did not invent them.
Further reading
- Reading Google Business Profile as JSON-LD — how GBP maps onto schema.org in the first place, which is the surface this article’s provenance discussion is anchored against.
- The Three Provenance Paths for AI Assistants — first-party schema vs Knowledge Graph vs third-party republish, applied generally to all entity attributes including ratings.
- NAP Consistency at the Entity Reconciliation Level — why
sameAslinkages between your site, your GBP, and your platform listings are the glue that lets the four-source rating reconciliation in this article work at all. - LLMO Framework — Structure & Provenance axes, where AI Native MEO is listed as reference implementation #1, and the LLMO vs SEO / AEO / GEO terminology guide that places the reputation citation layer inside the larger framework.
Frequently asked questions
- How do I generate JSON-LD for reviews and aggregateRating?
- Attach both aggregateRating and a review array to the same LocalBusiness (or Restaurant, Bakery, CafeOrCoffeeShop, Store) parent node. aggregateRating carries ratingValue, reviewCount, bestRating, and worstRating. Each Review child needs an author, a datePublished, and its own reviewRating typed as Rating — not AggregateRating. This article ships copy-editable templates for restaurant, cafe, and retail verticals; the graph shape is what matters, not the prose in reviewBody.
- What is the difference between aggregateRating and individual Review objects?
- aggregateRating is a summary statistic — a single ratingValue plus reviewCount attached to the business node. Each Review is a typed observation with its own author, date, rating, and reviewBody. Parsers treat an aggregate with no underlying Review children as a claimed statistic; the same aggregate with three to five Review children whose mean roughly matches becomes a corroborated one. The presence of the underlying graph is what bumps the assertion from claimed to internally corroborated.
- Whose rating wins when my JSON-LD and Google Business Profile disagree?
- Google Business Profile is the anchor. If first-party JSON-LD says 312 reviews and GBP says 287, assistants treat the GBP figure as canonical and the site schema as a possibly-stale mirror. The direction of the mismatch does not matter — GBP wins either way. The remediation is not to argue with Google; it is to stop letting reviewCount drift more than a week behind the GBP figure, since the disagreement is almost always a count mismatch rather than a value mismatch.
- Why does aggregateRating alone lose trust with AI assistants?
- Because a summary statistic with no underlying observations attached is unverifiable from the parser's point of view. Anyone can write '4.9 (1,247)' into a JSON block. Once an array of Review children with authors, dates, and individual ratings sits under the same parent — and their mean roughly matches the aggregate — the assertion moves from claimed to internally corroborated. That internal-consistency signal is what retrieval pipelines can score cheaply and weight heavily.
- How recent do Review.datePublished dates need to be?
- Recent enough that the newest entry is not multiple years old. A review array whose most recent entry is from 2023 reads as a fossilized testimonial page and depresses the trust signal even when aggregateRating is high. datePublished is also the axis engines sort on when they pick which review body to lift into an answer, so a stale array is silently sacrificing the quotable surface as well as the freshness signal.