Fixing Product Schema Errors
Product markup tells search engines what you sell, what it costs, whether it is in stock and how it ships. Google uses it for two different rich results: product snippets, with price and stars under a listing, and merchant listings, the shopping-style results with images, prices and delivery details.
Each has its own required fields and its own report in Search Console, which is why the same product page can pass one and fail the other. This guide covers the errors behind both, with the exact fix for each and a complete example you can copy.
Two rich results from one Product
Product is the schema.org type for anything you sell: a physical item, a digital download
or a product variant. Google reads it for two separate rich results, and the Rich Results Test and Search
Console report them separately:
| Product snippets | Merchant listings | |
|---|---|---|
| What it shows | Price, availability and stars under an ordinary search result. | Shopping-style results: image, price, delivery, returns, in the Shopping tab, image search and product panels. |
| For which pages | Any page about a product, including reviews and comparisons. | Only pages where the reader can buy the product from you. |
| Required | name, plus one of offers, review or aggregateRating. | name,
image, and offers with price and priceCurrency. |
| Search Console report | Product snippets | Merchant listings |
A review site that writes about a product it does not sell should aim for product snippets only. A
shop should aim for both. When a shop page fails merchant listings but passes product snippets, the gap
is almost always image, a missing or badly formatted price, or the shipping
and returns fields.
When Product is the wrong type
- A category or listing page with many products is not a Product. Mark up each product on its own page.
- A service such as a boiler repair or an SEO audit is a
Service, not a Product, and does not get product rich results. - A product that is not for sale anywhere, such as a discontinued model kept for reference, should not carry an offer with a price.
The fields Product needs
| Field | Needed for | What it holds |
|---|---|---|
name | Both (required) | The product name as shown on the page. |
image | Merchant listings (required) | One or more full https addresses of product photos Google can crawl. |
offers | Merchant listings (required); snippets (one of three) | An
Offer, or an AggregateOffer for a price range from several sellers. |
offers.price and offers.priceCurrency | Merchant listings (required) | A
number such as 34.00 and a code such as GBP. |
offers.availability | Recommended | A schema.org URL: https://schema.org/InStock,
OutOfStock, PreOrder, BackOrder. |
offers.shippingDetails | Merchant listings (recommended) | An OfferShippingDetails:
cost, destination and delivery time. |
offers.hasMerchantReturnPolicy | Merchant listings (recommended) | A
MerchantReturnPolicy: return window, method and fees. |
brand, sku, gtin, mpn | Recommended | Identifiers that let Google match the product to the same item elsewhere. |
aggregateRating, review | Snippets (one of three) | Real ratings and reviews collected on your own site and shown on the page. |
description | Recommended | A plain-text description of the product. |
A correct block you can copy
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Oak chopping board, large",
"description": "Solid oak end-grain chopping board, 45 x 30 x 4 cm, oiled and ready to use.",
"image": [
"https://example.com/images/oak-board-1x1.jpg",
"https://example.com/images/oak-board-4x3.jpg",
"https://example.com/images/oak-board-16x9.jpg"
],
"sku": "NW-OAK-L",
"gtin13": "5012345678900",
"brand": { "@type": "Brand", "name": "Northwood" },
"offers": {
"@type": "Offer",
"url": "https://example.com/oak-board",
"price": "34.00",
"priceCurrency": "GBP",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition",
"shippingDetails": {
"@type": "OfferShippingDetails",
"shippingRate": { "@type": "MonetaryAmount", "value": "3.95", "currency": "GBP" },
"shippingDestination": { "@type": "DefinedRegion", "addressCountry": "GB" },
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 1, "unitCode": "DAY" },
"transitTime": { "@type": "QuantitativeValue", "minValue": 1, "maxValue": 3, "unitCode": "DAY" }
}
},
"hasMerchantReturnPolicy": {
"@type": "MerchantReturnPolicy",
"applicableCountry": "GB",
"returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
"merchantReturnDays": 30,
"returnMethod": "https://schema.org/ReturnByMail",
"returnFees": "https://schema.org/FreeReturn"
}
}
}
</script>
The errors you will see, and what each one means
| Message | Cause | Fix |
|---|---|---|
| Either "offers", "review", or "aggregateRating" should be specified | A Product with a name and nothing else, often from a theme that outputs a bare Product on every page. | Add the offer, or remove the Product block from pages that do not sell it. |
| Missing field "image" (merchant listings) | No image, or a relative path. | Add full https image addresses. |
| Missing field "price" / Invalid price | The price is empty, contains a currency sign ("£34"), a thousands comma ("1,299.00") or a decimal comma ("34,00"). | Write 34.00 or 1299.00:
digits and a full stop only. |
| Missing field "priceCurrency" / Invalid currency | No currency, or a sign or word ("£", "pounds") in place of the code. | Use the ISO 4217 code: GBP, EUR, USD. |
| Invalid value for field "availability" | Plain text such as "In stock" or "instock". | Use
the schema.org URL: https://schema.org/InStock. |
| Missing field "shippingDetails" | No shipping information in the offer (a warning for merchant listings). | Add an OfferShippingDetails, or set shipping at account level in Merchant Center. |
| Missing field "hasMerchantReturnPolicy" | No returns information (a warning for merchant listings). | Add a MerchantReturnPolicy to the offer or to your Organization. |
| Invalid object type for field "brand" | Brand written as a bare string in some templates, or as an Organization with no name. | Use { "@type": "Brand", "name": "..." }. |
| Duplicate Product | The platform, the theme and a review app each output their own Product. | Keep
one complete Product and switch off the others, or merge them with the same @id. |
Price, currency and availability
Price problems cause more failed merchant listings than anything else. The rules are strict because Google compares the markup with the price on the page and in your product feed.
| Wrong | Right | Why |
|---|---|---|
"price": "£34.00" | "price": "34.00" | The currency goes in priceCurrency, not in the price. |
"price": "1,299.00" | "price": "1299.00" | No thousands separators. |
"price": "34,00" | "price": "34.00" | A full stop for decimals, whatever the site's language. |
"price": "from 20" | AggregateOffer with lowPrice | A range needs AggregateOffer. |
"priceCurrency": "£" | "priceCurrency": "GBP" | A three-letter ISO 4217 code. |
"availability": "In stock" | "availability": "https://schema.org/InStock" | Availability is a fixed list of schema.org values. |
Sale prices
Mark up the price the buyer pays now. If the page shows a sale price with the old price crossed out,
price is the sale price. Google supports showing the old price through a priceSpecification
with "priceType": "https://schema.org/StrikethroughPrice"; do not put the old price in price.
Prices with and without VAT
For UK shoppers, mark up the price including VAT, the same as the page shows. A trade site that shows
prices before VAT should add a UnitPriceSpecification with valueAddedTaxIncluded
set to false.
Out-of-stock products
Keep the Product and Offer, and change availability to https://schema.org/OutOfStock.
Removing the offer altogether turns a valid merchant listing into an error on every out-of-stock page.
Variants, price ranges and several sellers
Sizes and colours
A product sold in several sizes or colours can be marked up as a ProductGroup with one
Product per variant in hasVariant, and variesBy naming what changes,
such as https://schema.org/size. Each variant has its own sku, offers
and, where it differs, image. Give the group a productGroupID.
{
"@type": "ProductGroup",
"name": "Merino crew jumper",
"productGroupID": "MCJ",
"variesBy": ["https://schema.org/size", "https://schema.org/color"],
"hasVariant": [
{ "@type": "Product", "sku": "MCJ-S-NAVY", "size": "S", "color": "Navy",
"offers": { "@type": "Offer", "price": "65.00", "priceCurrency": "GBP", "availability": "https://schema.org/InStock" } },
{ "@type": "Product", "sku": "MCJ-M-NAVY", "size": "M", "color": "Navy",
"offers": { "@type": "Offer", "price": "65.00", "priceCurrency": "GBP", "availability": "https://schema.org/OutOfStock" } }
]
}
Price ranges and marketplaces
When several sellers offer the same product, or the page lists a range without choosing a variant,
use an AggregateOffer with lowPrice, highPrice, priceCurrency
and offerCount. AggregateOffer works for product snippets; merchant listings need a single
Offer the reader can buy on your page.
Fixing JSON that cannot be read
An unparsable block drops the whole Product, and every other type in the same script. Product names and descriptions are full of characters that break JSON when a template joins strings instead of encoding them.
- Inch and foot marks.
32" TVends the string. Write32\" TVor "32-inch TV". - A trailing comma after the last field, often when a template skips an empty optional field.
- Curly quotes copied from a supplier's product description.
- Raw line breaks inside a description pulled from a rich text field.
- HTML in the description. Tags are allowed in the text but must not contain unescaped
double quotes:
<span class="x">breaks the block.
json_encode() in PHP, | json in Shopify Liquid, or JSON.stringify()
in JavaScript. All three escape quotes, line breaks and HTML for you.The fix, step by step
Step 1
Find every Product block on the page
Open the page source (Ctrl+U, or Cmd+Option+U on a Mac) and search for "Product".
On Shopify and WooCommerce there are often three: one from the platform or theme, one from a review app
and one from an SEO plugin. The Schema Debugger lists every block with its source position, so you can
see which one is the most complete and which ones to switch off.
Step 2
Fix JSON that cannot be read
Paste each block into the Schema Markup Validator. A parsing error gives the line and column; the fault is usually an inch mark, a quote or a raw line break in a product name or description pulled from the catalogue. Fix the template so it outputs the block with a JSON encoder, rather than editing one page, then paste the block in again until the validator reads it cleanly.
Step 3
Add the required fields
Make
sure every Product has name, image and offers, with the image
as a full https address that loads without a login. Remove Product markup from pages that do not sell
a product, such as category pages and blog posts, where it produces "Either offers, review, or aggregateRating
should be specified" errors. Give each product page one complete Product block. Re-test each template
after the change.
Step 4
Correct price, currency and availability
Check
that the price is digits and a full stop only, with no currency sign or thousands separator, that priceCurrency
is a three-letter code such as GBP, and that availability is a schema.org URL such as https://schema.org/InStock.
Compare each one with the price and stock status the page shows, and with your Merchant Center feed if
you send one. Fix any mismatch at the source.
Step 5
Add shipping, returns and identifiers
Add
shippingDetails and hasMerchantReturnPolicy to the offer, or a site-wide return
policy on your Organization, matching what your delivery and returns pages say. Add brand
as a Brand object, and sku, gtin or mpn where the product has
them, so Google can match your product to the same item sold elsewhere. Use real barcodes only: an invented
or mistyped GTIN does more harm than leaving the field out, and Merchant Center can reject the product
for it.
Step 6
Test and request a recrawl
Test the live URL in the Rich Results Test and open both Product snippets and Merchant listings, since an error can appear under one and not the other. When both are clean, use URL Inspection in Search Console and choose Request indexing, then press Validate fix on the Product snippets and Merchant listings reports so Google rechecks every corrected page. Re-test after app updates.
Stopping it from coming back
- One Product per page. Pick the source with the most complete markup, usually the platform or a dedicated schema app, and switch Product output off everywhere else.
- Read from live data. Price, stock and ratings in the markup should come from the same fields as the visible page, so a sale or a sell-out updates both at once.
- Encode, never join. Output the block with a JSON encoder, so product names with quotes or inch marks cannot break it.
- Keep out-of-stock offers. Change availability instead of removing the offer.
- Watch both reports. Product snippets and Merchant listings report separately in Search Console. Check both after theme or app updates.
1. What is the difference between product snippets and merchant listings?
Product snippets add price, availability and stars to an ordinary search result, and they work on any page about a product, including reviews and comparisons. Merchant listings are the shopping-style results with image, price, delivery and returns, shown in the Shopping tab, image search and product panels, and they only apply to pages where the reader can buy the product from you. Each has its own required fields and its own Search Console report.
2. Why does my product pass one test and fail the other?
Merchant listings need more than product snippets. Product snippets only need a name plus one of offers, review or aggregateRating. Merchant listings need a name, an image, and an offer with a price and a currency code. So a page with only a name and a rating passes product snippets and fails merchant listings. In the Rich Results Test, open both items to see which fields each one is missing.
3. How should I write the price?
Write the price as digits with a full stop for decimals and nothing else: no currency sign, no thousands separator and no words. For example, write 1299.00, not £1,299.00 or from 1299. Put the currency separately in priceCurrency as a three-letter ISO 4217 code such as GBP, EUR or USD. The price must be the one the buyer pays now, match the price on the page, and include VAT for UK shoppers.
4. Do I have to add shipping and returns?
They are recommended rather than required. Without them the page can still get a merchant listing, but Search Console shows missing field warnings and the listing carries less information for shoppers. Add shippingDetails and hasMerchantReturnPolicy to each offer, or set a return policy once on your Organization and shipping at account level in Merchant Center. Whichever you choose, the details must match what the page and your checkout actually say.
5. What should I do when a product is out of stock?
Keep the Product and its offer, and change availability to https://schema.org/OutOfStock. Removing the offer altogether turns a valid merchant listing into a missing offers error on every out-of-stock page, and the page loses its product result entirely. When stock returns, set availability back to InStock. For items that can be ordered before they arrive, use PreOrder or BackOrder instead, so shoppers see the right status in search. Keep the price in the offer while it is out of stock.
6. Can a category page have Product markup?
No. A category page lists many products and is not itself a product, so Product markup does not belong there. Mark up each product on its own product page instead. Product markup on category pages, often added by a theme that outputs a bare Product on every page, is one of the most common sources of Either offers, review, or aggregateRating should be specified errors in the Product snippets report.
7. How long before Search Console shows the fix?
After you request indexing with URL Inspection, Google usually recrawls the page within a few days. The Product snippets and Merchant listings reports update as each affected page is recrawled, which can take up to two weeks across a whole shop. Press Validate fix on each report once the pages are corrected so Google rechecks them as a group, and test each page straight away in the Rich Results Test rather than waiting.
The Schema Debugger lists every Product block on a page, checks price, currency and availability formats, flags missing shipping and returns, and compares the markup with the price and stock shown on the page.
Open Schema Debugger Fixes →