Skip to content

Product schema for shopping answers and rich results

How to write Product schema that Google accepts: Offer, price, availability, GTIN and brand, honest reviews, variants, and a price that matches the page.

By Crawlito · 8 min read

In short

  • Google treats product pages two ways: merchant listings for pages where a shopper can buy, and product snippets for pages that review or compare products without selling them.
  • A merchant listing needs a Product with name, image and an Offer whose price is greater than zero and whose priceCurrency is an ISO 4217 code.
  • Google's structured data guidelines say markup must be a true representation of the page, so a JSON-LD price that differs from the visible price is a defect to fix.
  • Review and aggregateRating markup is only for genuine ratings sourced from users and visible on the page; Google says not to aggregate ratings from other websites.
  • Google can read JSON-LD injected by JavaScript, but warns this makes Shopping crawls less frequent and less reliable, and crawlers that don't run JavaScript won't see it at all.
On this page

Product schema is a block of schema.org JSON-LD that states, in a fixed format, what a product page already shows: the product's name, image, brand and identifiers, and an Offer with its price, currency and availability. Google uses it for two separate things, merchant listings for pages that sell and product snippets for pages that review, and each has its own required fields. The rule under both is the same: the markup has to match what a visitor sees, down to the price.

This guide covers which of the two applies to your page, the properties that matter, how to mark up variants and reviews without breaking Google's guidelines, and how to check that the schema and the page agree.

Merchant listings or product snippets: which one is your page?

Google's Product structured data overview splits product pages by one question: can the visitor buy the product from you on this page?

Merchant listingsProduct snippets
For pages whereA shopper can buy the product from youPeople can't buy directly: editorial reviews, comparisons, aggregators
Requiredname, image, offers as a single Offer with price and priceCurrencyname, plus one of review, aggregateRating or offers
AggregateOffer allowedNo: the merchant has to be the sellerYes, with lowPrice and priceCurrency
Extra detailShipping, returns, sizing, member and sale pricesPros and cons on editorial review pages
Search Console reportMerchant listingsProduct snippets

The two overlap. Google says that adding the required merchant listing properties can also make a page eligible for product snippets, so a shop only needs one well-built block. A review site should not pretend to sell: merchant listing documentation says only pages where a shopper can purchase a product are eligible.

Two limits apply to both. Product rich results only support pages that focus on a single product or its variants, and Google recommends marking up product pages rather than category or listing pages. And enhancements such as ratings, shipping and price drops are shown at Google's discretion; valid markup makes a page eligible, it does not guarantee anything appears.

The Product and Offer properties that matter

Here is a minimal merchant listing for one product. The values are illustrative.

json
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Trail Runner 2 waterproof jacket, navy, size M",
  "image": [
    "https://shop.example.com/img/trail-runner-2-navy-1x1.jpg",
    "https://shop.example.com/img/trail-runner-2-navy-4x3.jpg",
    "https://shop.example.com/img/trail-runner-2-navy-16x9.jpg"
  ],
  "description": "Lightweight waterproof running jacket with taped seams.",
  "sku": "TR2-NVY-M",
  "gtin13": "5012345678900",
  "brand": { "@type": "Brand", "name": "Example Outdoor" },
  "offers": {
    "@type": "Offer",
    "url": "https://shop.example.com/trail-runner-2?color=navy&size=m",
    "price": 89.00,
    "priceCurrency": "EUR",
    "availability": "https://schema.org/InStock",
    "itemCondition": "https://schema.org/NewCondition"
  }
}

What each part needs, according to Google's merchant listing documentation:

  • price and priceCurrency are required on the Offer. The price must be greater than zero and the currency is an ISO 4217 code such as EUR or USD. If you sell in several currencies, Google asks for a distinct URL per currency.
  • availability is recommended and takes one value, for example InStock, OutOfStock, PreOrder, BackOrder or SoldOut. Google accepts short names, but the full https://schema.org/InStock URL form is the unambiguous choice.
  • image is required. Google recommends several high-resolution images (at least 50,000 pixels, width times height) in 16x9, 4x3 and 1x1 ratios.
  • Identifiers (gtin, gtin8, gtin12, gtin13, gtin14, mpn, sku) are recommended. Google says to give the GTIN in numerical form, not as a URL, and to use the most specific GTIN that applies. Only add a GTIN the manufacturer actually assigned.
  • brand.name is recommended. Nest it as a Brand object as above.
  • priceValidUntil is optional, but Google warns a listing may not display if it holds a past date. A hard-coded date from a sale two years ago quietly removes eligibility.

Sale prices, shipping and returns

For a sale, keep the active price in price with no priceType, and add a second price in priceSpecification with priceType set to https://schema.org/StrikethroughPrice. Google then treats the active price as the sale price. If both offers.price and offers.priceSpecification give an active price, Google uses offers.price.

shippingDetails and hasMerchantReturnPolicy are recommended on the Offer. If you also set shipping or returns in Merchant Center, Google documents an order of precedence: product-level Merchant Center feeds come first, then Content API for Shopping settings, then Merchant Center or Search Console settings, then product-level page markup, and organization-level markup last. Pick one source of truth per policy so they can't contradict each other.

Reviews and ratings: only if they are genuine

Ratings are the property people are most tempted to invent, and the one Google's rules are strictest about. From Google's review snippet guidelines:

  • Ratings must be sourced directly from users.
  • Don't include fake or undisclosed incentivized reviews, on the page or in the markup.
  • Don't aggregate reviews or ratings from other websites.
  • If you use aggregateRating, users should be able to see that rating on the page.
  • An aggregateRating needs ratingValue and at least one of ratingCount or reviewCount.

So the safe pattern is simple: mark up the reviews your own customers left, that the page displays, and nothing else. If a product has no reviews, leave review and aggregateRating out. A merchant listing does not need them; the Offer already satisfies the requirement.

json
{
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": 4.6,
    "reviewCount": 38
  }
}

If that block says 38 reviews, the page should show 38 reviews and the same average. Editorial review pages have one extra option: positiveNotes and negativeNotes for pros and cons, with at least two statements in total. Google says only editorial product review pages are eligible for the pros and cons appearance in Search.

Google's general structured data guidelines add that spammy markup can lead to a manual action. A structured data manual action costs the page its rich result eligibility, though Google says it does not affect ranking, and it shows up in the Manual Actions report in Search Console.

How to mark up product variants

Colours and sizes are separate products to Google, grouped under a ProductGroup. The product variants documentation describes the pattern:

json
{
  "@context": "https://schema.org",
  "@type": "ProductGroup",
  "name": "Trail Runner 2 waterproof jacket",
  "productGroupID": "TR2",
  "brand": { "@type": "Brand", "name": "Example Outdoor" },
  "variesBy": ["https://schema.org/color", "https://schema.org/size"],
  "hasVariant": [
    {
      "@type": "Product",
      "name": "Trail Runner 2 waterproof jacket, navy, size M",
      "sku": "TR2-NVY-M",
      "color": "Navy",
      "size": "M",
      "offers": {
        "@type": "Offer",
        "url": "https://shop.example.com/trail-runner-2?color=navy&size=m",
        "price": 89.00,
        "priceCurrency": "EUR",
        "availability": "https://schema.org/InStock"
      }
    },
    {
      "@type": "Product",
      "name": "Trail Runner 2 waterproof jacket, red, size M",
      "sku": "TR2-RED-M",
      "color": "Red",
      "size": "M",
      "offers": {
        "@type": "Offer",
        "url": "https://shop.example.com/trail-runner-2?color=red&size=m",
        "price": 89.00,
        "priceCurrency": "EUR",
        "availability": "https://schema.org/OutOfStock"
      }
    }
  ]
}

The rules that trip people up:

  1. Each variant needs a unique ID, such as its own sku or gtin.
  2. Each variant needs a URL that preselects it, usually a query parameter, and preselection should show that variant's image, price and availability.
  3. variesBy takes schema.org URLs from a short list: color, size, suggestedAge, suggestedGender, material and pattern.
  4. On a single-page setup, there must be one canonical URL for the whole group. On a multi-page setup, each page repeats the full ProductGroup. The parameter URLs and the canonical have to agree, which is where canonical tags and duplicate URLs cause trouble.
  5. Don't use AggregateOffer for variants. Google's product snippet documentation says not to use it to describe a set of product variants, and merchant listings require a single Offer in any case.

Why a price mismatch is a finding

Google's guidelines say structured data must be a true representation of the page content, and not describe anything readers can't see. The merchant listing documentation adds that Google may try to verify product data before showing it. A JSON-LD price of 79.00 on a page that shows 89.00 fails both tests.

Mismatches are rarely deliberate. They come from:

  • a theme or plugin that writes the base price into JSON-LD while the page shows a sale or member price;
  • a cached HTML page whose JSON-LD is hours older than the price the browser fetches;
  • tax: the schema has the net price, the page shows the gross price (or the reverse);
  • one Offer on a page where the selected variant has a different price;
  • availability stuck on InStock after stock runs out.

How to check it

Fetch the raw HTML the way a crawler does and pull out the JSON-LD:

bash
curl -s -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36; compatible; OAI-SearchBot/1.4; +https://openai.com/searchbot" \
  "https://shop.example.com/trail-runner-2" \
  | grep -o '<script type="application/ld+json">[^<]*</script>' \
  | sed 's/<[^>]*>//g' | jq '.. | objects | select(has("price")) | {price, priceCurrency, availability}'

The user agent is OpenAI's documented OAI-SearchBot string (the version number changes over time), so you see what your server and CDN return to that token; requests from your own IP are not verified OpenAI traffic, so a bot rule keyed on IP ranges may still answer differently. Compare those values with the price and stock message on the page itself. If grep finds nothing, the JSON-LD may be split over several lines or, more often, added by JavaScript after load. Raw vs rendered HTML shows how to compare the two versions.

Then run the URL through Google's Rich Results Test, which reports missing required properties, and watch the Merchant listings and Product snippets reports in Search Console. Neither checks that the markup matches the visible price, so that comparison is yours to make. Common JSON-LD errors covers the syntax problems that make a block unreadable in the first place.

How to fix it

Generate the JSON-LD from the same data, in the same server render, as the visible price. If the page shows a member price or tax-inclusive price, the markup should describe that same price, using priceSpecification for any second price. Make sure your cache is purged for both together when a price or stock level changes.

Server-rendered schema, shopping answers and AI crawlers

Google's page on JavaScript-generated structured data says Google can process JSON-LD that appears in the DOM when it renders the page, but warns that dynamically generated markup can make Shopping crawls less frequent and less reliable, which matters most for price and availability.

Other crawlers are less forgiving. Vercel's analysis with MERJ, published in December 2024, found that none of the major AI crawlers it measured, including OpenAI's crawlers, ClaudeBot and PerplexityBot, executed JavaScript at the time. JSON-LD added in the browser is not there for them.

Be realistic about what markup does for AI answers. Google says no special schema.org structured data is required to appear in AI Overviews or AI Mode; the technical requirements are ordinary Search eligibility. OpenAI's help article on shopping in ChatGPT search describes product results as drawing on structured metadata from third-party providers and merchants, such as price and description; at the time of writing it does not say ChatGPT reads schema.org markup from your pages. Product schema is clean, consistent data any system can parse. It is not a switch for being recommended. Structured data for AI search goes through what helps and what doesn't.

Frequently asked questions

Is Product schema required to appear in Google Shopping?

Not by itself. You can supply product data through page markup, a Merchant Center feed or both, and Google says providing both maximizes eligibility and helps it verify your data. For the Shopping tab, Google points to separate Merchant Center eligibility requirements.

Can I use AggregateOffer on my own product page?

Not for a merchant listing. Google requires a single Offer because the merchant has to be the seller, and it says not to use AggregateOffer to describe product variants. AggregateOffer fits product snippets on pages that compare several sellers.

Should I add star ratings if my product has no reviews?

No. Leave review and aggregateRating out. Google says ratings must come from users and be visible on the page, and a merchant listing is valid without them.

What happens if my schema price doesn't match the page?

Google's guidelines require markup to be a true representation of the page, and Google may verify product data before showing it, so the listing can lose eligibility. Fix it at the source by generating both from the same data.

Check your product pages

Pick your best-selling product and one with variants, and run the curl check above on each. Crawlito's free check reads one public page with no account needed. The full audit covers up to 500 pages with deterministic checks and human review, and delivers a PDF with the evidence and prioritized fixes.

Filed under Structure and schema. Topics: structured-data, json-ld, google, shopify, checklist