SmartEcomSEO
Technical SEO

Shopify Structured Data: Product, Offer, Return Policy and Shipping Schema

What Shopify themes output as structured data, what Google wants for merchant listings, and how to add return policy, shipping and variant markup in Liquid.

Illustration for “Shopify Structured Data: Product, Offer, Return Policy and Shipping Schema”
Key takeaways
  • Shopify's structured_data filter outputs Product for products without variants and ProductGroup for products with variants, with price, currency and availability per variant.
  • On the Dawn output we checked, there was no return policy, shipping, item condition, ratings or variesBy. Those are the gaps to fill.
  • Google recommends declaring your return and shipping policy once, under OnlineStore or Organization markup, rather than repeating it on every product.
  • Merchant Center and Search Console settings override product markup, which overrides organization markup. Keep all of them consistent.
  • One Product entity per page. Review apps that add a second Product block split your data; pull ratings from the reviews.rating metafields instead.

Shopify structured data is one of those areas where the platform gets you most of the way and then stops. Every modern theme outputs Product markup. Almost none output the return policy and shipping details Google asks for in merchant listings, and many stores end up with two or three conflicting Product blocks once review and SEO apps join in.

This guide covers what Shopify outputs by default, what Google wants, and how to fill the gaps in Liquid. We read Shopify's Liquid documentation and Google Search Central's merchant listing, return policy, shipping policy and product variant documentation, and pulled the JSON-LD from Shopify's Dawn demo store, in September 2026.

What Shopify outputs by default#

Themes built on Dawn render product markup with one line:

liquidliquid
<script type="application/ld+json">
  {{ product | structured_data }}
</script>

Shopify's docs say the filter works on product and article objects. Products "are output as a schema.org Product if they have no variants, and a ProductGroup if they have one or more variants." Articles become Article.

Here's what the Dawn demo product page contained when we pulled it:

PropertyPresentNotes
@type ProductGroup with hasVariantYesOne nested Product per variant
name, description, url, brandYesBrand comes from the product vendor
productGroupIDYesThe product ID
categoryYes, but emptyFilled from the product category when set
Variant sku, gtin, image, nameYesGTIN from the barcode field
Offer price, priceCurrency, availability, urlYesPer variant, with ?variant= URLs
variesByNoGoogle recommends it for variant groups
Variant color / sizeNoRecommended by Google
itemConditionNoRecommended for merchant listings
hasMerchantReturnPolicy, shippingDetailsNoRecommended for merchant listings
aggregateRating, reviewNoUsually added by a review app
BreadcrumbListNoNot on the product page we checked

Dawn also printed an Organization block (name, logo, sameAs, url) on the pages we checked. Your theme may output more or less, so check before you change anything: view source and search for application/ld+json, or run the page through Google's Rich Results Test.

What Google wants for merchant listings#

Google's merchant listing documentation has two layers.

Required: name, image and offers on the Product, and price (greater than zero) and priceCurrency on the Offer. Shopify's default covers these.

Recommended: availability, itemCondition, shippingDetails, hasMerchantReturnPolicy, identifiers like gtin, brand, and review data. These are the fields that let Google show "free returns" or delivery costs next to your products.

For variants, Google's product variant guide recommends productGroupID, variesBy and hasVariant on the ProductGroup, and properties like color and size on each variant. Shopify stores are what Google calls "single-page" variant sites: one canonical URL for the group, with ?variant= preselecting each option.

Return policy and shipping: do it once, at the organization level#

You don't need to repeat your return policy on 5,000 products. Google's return policy documentation says a standard policy that applies to most or all products can be nested under Organization with hasMerchantReturnPolicy, and shipping works the same way with hasShippingService. Google recommends placing each on a single page rather than every page, and recommends the OnlineStore subtype for ecommerce sites.

The precedence order matters, because it tells you which source wins when they disagree. From strongest to weakest, per Google's documentation:

  1. Content API for Shopping
  2. Merchant Center or Search Console settings
  3. Product-level markup
  4. Organization-level markup

So if you sync products to Merchant Center (for example through Shopify's Google & YouTube app), set your return and shipping policies there too. The markup is the fallback for everything Merchant Center doesn't cover, and it's what other systems reading your pages see.

Liquid: an OnlineStore block with returns and shipping#

This goes in theme.liquid, output only on the home page. It reads the policy numbers from shop metafields, so your team can change them in the admin without touching code. Create the metafields first under Settings > Custom data > Shop.

liquidliquid
{%- if request.page_type == 'index' -%}
{%- liquid
  assign return_days = shop.metafields.custom.return_days.value | default: 30
  assign return_country = shop.metafields.custom.return_country.value | default: 'US'
-%}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "OnlineStore",
  "name": {{ shop.name | json }},
  "url": {{ request.origin | json }},
  "hasMerchantReturnPolicy": {
    "@type": "MerchantReturnPolicy",
    "applicableCountry": ["US", "CA"],
    "returnPolicyCountry": {{ return_country | json }},
    "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
    "merchantReturnDays": {{ return_days | json }},
    "returnMethod": "https://schema.org/ReturnByMail",
    "returnFees": "https://schema.org/ReturnShippingFees",
    "merchantReturnLink": {{ shop.refund_policy.url | prepend: request.origin | json }}
  },
  "hasShippingService": {
    "@type": "ShippingService",
    "name": "Standard freight delivery",
    "shippingConditions": [{
      "@type": "ShippingConditions",
      "shippingDestination": { "@type": "DefinedRegion", "addressCountry": "US" },
      "shippingRate": { "@type": "MonetaryAmount", "value": 0, "currency": "USD" },
      "transitTime": {
        "@type": "ServicePeriod",
        "duration": { "@type": "QuantitativeValue", "minValue": 5, "maxValue": 10, "unitCode": "DAY" }
      }
    }]
  }
}
</script>
{%- endif -%}

Adjust the countries, fees and transit times to your real policy. If your theme already prints an Organization block, merge the two into one entity instead of adding a second.

For high-ticket stores, use the fields that reflect how you really ship: freight delivery with longer transit times, return shipping fees or a restockingFee on large items. Accurate beats attractive. Markup that promises free returns your policy doesn't offer is a mismatch waiting to be found.

Product-level markup: when the default isn't enough#

The structured_data filter returns a finished JSON object, so you can't add properties to it. You have two choices:

  1. Keep the filter and cover returns and shipping at the organization level (above). This is the right answer for most stores.
  2. Replace the filter with your own JSON-LD when you need per-product shipping (freight vs parcel), variesBy, itemCondition or ratings in the same entity.

If you replace it, here is a compact ProductGroup we use as a starting point:

liquidliquid
{%- liquid
  assign rating = product.metafields.reviews.rating.value
  assign rating_count = product.metafields.reviews.rating_count.value
-%}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "ProductGroup",
  "name": {{ product.title | json }},
  "description": {{ product.description | strip_html | truncatewords: 60 | json }},
  "url": {{ product.url | prepend: request.origin | json }},
  "brand": { "@type": "Brand", "name": {{ product.vendor | json }} },
  "productGroupID": {{ product.id | json }},
  {%- if rating and rating_count > 0 %}
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": {{ rating.rating | json }},
    "bestRating": {{ rating.scale_max | json }},
    "ratingCount": {{ rating_count | json }}
  },
  {%- endif %}
  "hasVariant": [
    {%- for variant in product.variants %}
    {
      "@type": "Product",
      "name": {{ product.title | append: ' - ' | append: variant.title | json }},
      "sku": {{ variant.sku | json }},
      {%- if variant.barcode != blank %}"gtin": {{ variant.barcode | json }},{% endif %}
      "image": {{ variant.image | default: product.featured_image | image_url: width: 1200 | prepend: 'https:' | json }},
      "offers": {
        "@type": "Offer",
        "url": {{ variant.url | prepend: request.origin | json }},
        "price": {{ variant.price | divided_by: 100.0 | json }},
        "priceCurrency": {{ cart.currency.iso_code | json }},
        "itemCondition": "https://schema.org/NewCondition",
        "availability": "https://schema.org/{% if variant.available %}InStock{% else %}OutOfStock{% endif %}"
      }
    }{% unless forloop.last %},{% endunless %}
    {%- endfor %}
  ]
}
</script>

Add variesBy and per-variant color or size if your options map cleanly to them; Google expects schema.org values such as https://schema.org/color, so an option like "Wood finish" doesn't fit. If you use this block, remove the structured_data line from the product section, or you'll ship two Product entities.

The reviews.rating and reviews.rating_count metafields are Shopify standard definitions that review apps are meant to write to. Check that yours does before relying on them.

One Product entity per page#

This is the most common structured data problem we find on Shopify, and it's usually caused by apps:

  • The theme outputs a ProductGroup.
  • The review app outputs a second Product with only aggregateRating.
  • An SEO app outputs a third, sometimes with a different price or currency.

Google has to reconcile them, and the Rich Results Test often shows duplicate or partial items. Pick one source of truth. Either let one app own product markup and remove the theme's line, or keep the theme's markup, turn off the apps' schema output, and bring ratings in through the metafields.

Other types worth having#

TypeWhereWhy
OnlineStore / OrganizationHome pageName, logo, sameAs, returns and shipping
BreadcrumbListProducts and collectionsShows hierarchy that Shopify URLs don't
ArticleBlog postsDawn applies the same structured_data filter to articles
ItemListCollections (optional)Some teams add it; Google doesn't show a rich result for it on category pages
FAQPagePages with real FAQsGoogle limits FAQ rich results to authoritative government and health sites, so treat it as optional

Shopify URLs don't show hierarchy, so breadcrumbs do that job. A minimal collection-page version for theme.liquid:

liquidliquid
{%- if template.name == 'collection' -%}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "Home",
      "item": {{ request.origin | append: routes.root_url | json }} },
    { "@type": "ListItem", "position": 2, "name": {{ collection.title | json }},
      "item": {{ collection.url | prepend: request.origin | json }} }
  ]
}
</script>
{%- endif -%}

On product pages, use the same primary-collection logic as your visible breadcrumb, so the markup and the page agree. The pattern is in Shopify URL structure.

For AI assistants, markup helps less than you might hope. Shopify's AI channels read product data from your admin (category, metafields, descriptions), and ChatGPT or Perplexity read the page itself. Clean HTML with specs in text matters as much as JSON-LD. More in how to rank in ChatGPT.

Errors we see most on Shopify catalogs#

Report message or symptomUsual cause on ShopifyFix
Missing field "hasMerchantReturnPolicy"Theme default has no return dataOrganization-level return policy
Missing field "shippingDetails"SameOrganization-level hasShippingService
Invalid or zero price"Request a quote" or sample products priced at 0Don't output an Offer for quote-only products
Invalid GTINInternal codes typed into the barcode fieldClear the barcode or enter the real GTIN
Two Product items on one URLReview or SEO app adding its own blockOne source of truth
Price mismatch in Merchant CenterMarket currency or sale price differs between feed and pageCheck the page price per market

Two of these hit our ideal clients hardest. High-ticket stores often sell some items by quote, and a price of zero fails merchant listing requirements; leave the Offer out for those products and make the page's enquiry path clear instead. Chinese brands moving from Amazon often carry internal FNSKU or factory codes in the barcode field, which then appear as invalid GTINs across the catalog.

How to test#

  1. Rich Results Test on one product with variants, one without, one collection, one article and the home page.
  2. Search Console Merchant listings and Product snippets reports, for errors and warnings across the catalog.
  3. Merchant Center diagnostics, for price and availability mismatches.
  4. After every app install, view source and count application/ld+json blocks again.

Where this fits#

Structured data won't rank a page on its own. It makes sure Google reads your price, stock, returns and shipping correctly, which is what merchant listings and Shopping surfaces run on. Get crawl and canonicals right first (the Shopify technical SEO guide and Shopify SEO checklist), then fill the schema gaps. It's a standard part of our Shopify technical SEO work.

People also ask

Questions people ask about this

Does Shopify add structured data automatically?

Most themes do. Dawn and themes built on it output {{ product | structured_data }}, which becomes a Product or a ProductGroup with variants, including brand, SKU, GTIN, price, currency and availability. On the Dawn output we checked there was no return policy, shipping details, item condition, ratings or breadcrumb markup, so those usually need adding in the theme.

How do I add return policy schema to Shopify?

The simplest way is organization-level markup: an OnlineStore or Organization JSON-LD block on your home page with hasMerchantReturnPolicy, including applicableCountry, returnPolicyCategory, merchantReturnDays, returnMethod and returnFees. Google recommends this over repeating the policy on every product. If you use Merchant Center, set the same policy there, because Merchant Center settings take precedence.

Do I need shippingDetails on every Shopify product?

No. Google supports a shipping policy declared once with hasShippingService under Organization or OnlineStore. Product-level shippingDetails are only needed when products ship differently, for example freight for large items and parcel for accessories. Merchant Center shipping settings override both, so keep them consistent with your markup and your written shipping policy.

Why does my Shopify product have two Product schema blocks?

Usually because a review or SEO app adds its own Product markup alongside the theme's. Google then sees duplicate or partial items. Choose one source: remove the theme's structured_data line and let one app own it, or keep the theme markup, turn off app schema output, and pull ratings from the reviews.rating and reviews.rating_count metafields.

Does structured data help Shopify stores appear in AI search?

Somewhat. Accurate markup helps systems read price, availability and policies, but AI assistants also read the visible page, and Shopify's AI channels use product data from your admin, such as categories and metafields. Clear specs in the page text, complete product data and crawlable pages matter at least as much as JSON-LD.

Next step

Want this done on your store?

Clean Product, ProductGroup, Offer, return, shipping, Organization and breadcrumb markup for Shopify, aligned with your Merchant Center feed for free listings.

Talk to Zack directlyUsually replies within 24 hours
WeChatSearch this ID in WeChat
WhatsApp +86 186 8214 2136