Structured data mastery

Product schema for Shopify

Every Schema.org Product property that matters for AI surfacing, with Shopify Liquid examples and the variants that go wrong most often in production.

10 min read Updated May 1, 2026

Schema.org Product defines more than sixty properties directly, plus more inherited from Thing. Most Shopify themes ship four. The gap between four and the dozens that matter for AI parsing is the gap between “present” and “parsed.” This guide walks through the properties that matter, the Liquid that produces them correctly, and the seven failure modes that show up most often in production catalogs.

This is a reference, not a tutorial. If a property does not change behavior in any of the major AI agents or Google rich results, it is not in this guide. If something in this guide contradicts schema.org/Product, schema.org wins — the spec is the source of truth.

The product-schema flow on a typical Shopify storefront looks like this — one source of truth in Liquid, four downstream destinations:

Shopify product
+ variant + metafields

Liquid template
renders JSON-LD

Schema.org Product
in HTML head

Google Rich Results
+ AI Overviews

ChatGPT product index

Perplexity citations

Multi-modal models
Gemini, GPT-4V

Where the markup actually lives on a rendered Shopify product page — two readers, two places they look:

Where product schema lives on a Shopify pageA stylized browser-source view showing the head section containing the JSON-LD Product block and the body containing prose marketing content. Annotations show what each section contributes to indexing and surfacing.view-source: yourstore.com/products/wool-runner<head> <script type=“application/ld+json”> { “@context”: “https://schema.org”, “@type”: “Product”, “name”: “Wool Runner Mizzles”, “gtin13”: “0840062303849”, … }</head><body> <h1>Wool Runner Mizzles</h1> <p>Built for puddles. Our weather- ready merino wool runner is…</p></body>AI AGENTS READ FIRSTSchema.org Product— structured, parseable— deterministic fields— machine-validatedFALLBACK / SUMMARYProse marketing copy— inferred, not parsed— used when schema fails

The four every theme ships

Most stock Shopify themes (Dawn, Sense, Refresh, and the major paid ones — Impulse, Prestige, Empire) ship Product markup with these four properties:

This is enough to validate. It is not enough to parse usefully. Every major AI agent can read this markup; none give the product a discoverability lift over a competitor with fuller markup.

The properties that move surfacing

The properties that are load-bearing for AI agent behavior, in rough order of impact:

sku and gtin13 / gtin8 / mpn

Universal product identifiers. AI agents use these to deduplicate the same product across multiple stores and to verify that the product is authoritative. Catalogs without identifiers compete with their own resellers and lose.

Private label catalogs are the most common omission. Apply for GTINs through GS1; do not invent them.

{%- assign variant = product.selected_or_first_available_variant -%}
{
  "@context": "https://schema.org",
  "@type": "Product",
  "sku": {{ variant.sku | json }},
  {%- if variant.barcode -%}
  "gtin13": {{ variant.barcode | json }},
  {%- endif -%}
  "mpn": {{ variant.metafields.product.mpn | json }},
  ...
}

brand

Always include. Brand is a documented input for branded-product queries across Google Shopping, Bing Shopping, and the indexes that AI surfaces query. A missing brand removes the catalog from queries that name a brand alongside a product type.

"brand": {
  "@type": "Brand",
  "name": {{ product.vendor | json }}
}

offers (full)

The offers property is where most schema fails silently. The minimal version ships with price and priceCurrency. The full version includes:

"offers": {
  "@type": "Offer",
  "url": {{ shop.url | append: product.url | json }},
  "priceCurrency": {{ cart.currency.iso_code | json }},
  "price": {{ variant.price | divided_by: 100.0 | json }},
  "priceValidUntil": "{{ 'now' | date: '%Y' | plus: 1 }}-12-31",
  "availability": {%- if variant.available -%}
    "https://schema.org/InStock"
  {%- else -%}
    "https://schema.org/OutOfStock"
  {%- endif %},
  "itemCondition": "https://schema.org/NewCondition",
  "seller": {
    "@type": "Organization",
    "name": {{ shop.name | json }}
  }
}

aggregateRating and review

Rich results trigger only when there are real reviews. Faking ratings to game rich results gets the markup ignored at best and the page penalized at worst. If the product has fewer than five real reviews, omit these properties entirely.

{%- if product.metafields.reviews.rating_count > 4 -%}
"aggregateRating": {
  "@type": "AggregateRating",
  "ratingValue": {{ product.metafields.reviews.rating | json }},
  "reviewCount": {{ product.metafields.reviews.rating_count | json }}
},
{%- endif -%}

Google narrowed which review markup triggers rich results in 2024 — self-serving reviews stopped firing, and AggregateRating is now only valid on the page where the product is sold (not on hub or category pages that aggregate reviews from elsewhere).

category

Maps the product to a Google product category. Required for Google Merchant Center; optional but valued by other AI agents. Use the full taxonomy path, not just the leaf node.

"category": "Apparel & Accessories > Clothing > Outerwear > Coats & Jackets"

Variant handling

The single biggest cause of malformed Shopify product schema. Two patterns work; mixing them does not.

The short version: on a default product page (no variant in the URL), the schema represents the parent product with hasVariant listing each variant as a ProductGroup member. On a variant-specific URL (?variant=12345), the schema represents the specific variant.

Most stores ship the parent-product version on every URL, which is wrong: the agent reads the parent, sees no specific size or color, and treats the page as a category-level result rather than a specific recommendation. This is a common variant-handling failure pattern in Shopify catalogs.

Image properties

image accepts a single URL, an array of URLs, or ImageObject entries. The richer the markup, the better the multi-modal models do.

"image": [
  {%- for img in product.images -%}
    {{ img | img_url: '2048x2048' | prepend: 'https:' | json }}
    {%- unless forloop.last -%},{%- endunless -%}
  {%- endfor -%}
]

For multi-modal lift, use ImageObject with explicit dimensions:

"image": [
  {%- for img in product.images -%}
  {
    "@type": "ImageObject",
    "url": {{ img | img_url: '2048x2048' | prepend: 'https:' | json }},
    "width": {{ img.width }},
    "height": {{ img.height }}
  }{%- unless forloop.last -%},{%- endunless -%}
  {%- endfor -%}
]

Q&A pairs as a sibling FAQPage block

If the catalog has product-specific Q&A content (sizing, fit, compatibility, use-case clarification), FAQPage is its own Schema.org type — render it as a second JSON-LD block alongside the Product block, not nested inside it.

The Shopify pattern: store Q&A pairs in a metafield (a list of metaobjects with question and answer fields), then render in Liquid:

{%- if product.metafields.product.faq.value.size > 0 -%}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {%- for entry in product.metafields.product.faq.value -%}
    {
      "@type": "Question",
      "name": {{ entry.question | json }},
      "acceptedAnswer": {
        "@type": "Answer",
        "text": {{ entry.answer | json }}
      }
    }{%- unless forloop.last -%},{%- endunless -%}
    {%- endfor -%}
  ]
}
</script>
{%- endif -%}

The metafield definition (Settings → Custom data → Products → Add definition) is product.faq with type “list of metaobjects.” Each metaobject has a question (single line text) field and an answer (multi-line text) field.

This is the rendering pattern. Note that testing in late 2025 suggests that AI agents may not consistently extract JSON-LD on direct page fetch; the value of structured Q&A markup appears to come through index and feed paths (Google’s index → AI Overviews, etc.) rather than agents reading the page directly.

Seven failure modes

In rough order of frequency, the patterns that show up most often when auditing Shopify product schema in the wild:

  1. offers.price as a string. Shopify’s money filter outputs formatted currency ("$45.00"). Schema needs a number. Use variant.price | divided_by: 100.0 | json.

  2. availability as a free-form string. “InStock” is invalid; https://schema.org/InStock is correct. Same for OutOfStock, PreOrder, BackOrder.

  3. Multiple Schema.org Product blocks per page. Common when a theme ships product schema and an app injects more. AI agents pick one at random. Audit with view-source and the Schema.org Validator.

  4. Variant schema on parent URL. Page is the parent product page; schema describes one specific variant. Reads as a malformed product to most agents.

  5. sku shared across variants. Each variant should have a unique SKU; some catalogs reuse the parent SKU on every variant. Breaks inventory parsing in GMC.

  6. Stale priceValidUntil. Set to a date in the past; rich results stop firing. Use a dynamic Liquid expression, not a hardcoded date.

  7. description containing HTML. Schema’s description field is plain text. Stripping HTML on the way in: {{ product.description | strip_html | json }}.

The contrarian take

Most schema content treats Schema.org Product as a static reference — write the markup, validate, ship, done. The reality: schema is dynamic infrastructure. As Schema.org publishes new properties, as Google narrows what triggers rich results, as AI agents re-tune which properties they weight, the markup that scored well last quarter underperforms this quarter without changing.

The maintenance pattern: re-validate the catalog quarterly, audit against the current Schema.org spec annually, and treat the markup as a living layer rather than a one-time setup task.

Where this still won’t be enough

Three cases:

Validation

Three tools, in order of strictness:

  1. Google’s Rich Results Test (search.google.com/test/rich-results) — tells you whether rich results will trigger. Most useful for verifying your changes will show up in Google.
  2. Schema.org Validator (validator.schema.org) — tells you whether the markup conforms to the spec. Useful for catching property errors Google’s tester ignores.
  3. Manual JSON inspection. Pretty-print the JSON-LD from the rendered page, read it. The errors that pass both validators usually fall out of a manual read — null values that should be omitted, malformed nested objects, character-encoding issues.