Optimization Guide
Shopify ProductGroup & hasVariant Schema: Variant JSON-LD for AI Shopping Agents
Shopify's default JSON-LD sees your 12 color variants as one product with a price range. AI shopping agents see the same thing — and can't answer "does this come in navy, size L?" confidently. ProductGroup with hasVariant turns each variant into an individually addressable, structured entity.
ProductGroup containing a hasVariant array (one Product per variant). Set variesBy to https://schema.org/color, size, or material matching your Shopify option names. Each variant Product gets its own image, SKU, GTIN, color/size value, and Offer with availability and url using the ?variant=ID parameter. AI agents can then answer "do you have this in blue, size M?" with high confidence.
The Variant Problem in Shopify JSON-LD
Shopify's default product JSON-LD outputs a single Product with an Offers array — or an AggregateOffer with lowPrice and highPrice. Neither pattern communicates which specific variants are available, what colors and sizes exist, or which variant IDs correspond to which options.
For AI shopping agents processing purchase queries, this is a significant gap:
- "I need a red hoodie in XL" — agent can't confirm the red XL variant is in stock from the default JSON-LD
- "Show me this bag in tan leather" — material option isn't exposed in standard Product structured data
- "Does the blue one cost the same as the black?" — per-variant pricing differences are invisible
For product categories where variant selection is the primary purchase decision — apparel, footwear, furniture with fabric options, accessories — this is a critical missing layer.
ProductGroup Structure
{
"@context": "https://schema.org",
"@type": "ProductGroup",
"name": "Apex Fleece Hoodie",
"description": "Mid-weight 320GSM French Terry hoodie with kangaroo pocket.",
"url": "https://store.com/products/apex-fleece-hoodie",
"brand": {"@type": "Brand", "name": "Acme Apparel"},
"productGroupID": "apex-fleece-hoodie",
"variesBy": [
"https://schema.org/color",
"https://schema.org/size"
],
"hasVariant": [
{
"@type": "Product",
"name": "Apex Fleece Hoodie — Navy / S",
"sku": "AFH-NAVY-S",
"gtin13": "09501234567890",
"color": "Navy",
"size": "S",
"image": "https://cdn.shopify.com/products/apex-hoodie-navy-s.jpg",
"offers": {
"@type": "Offer",
"price": "79.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"url": "https://store.com/products/apex-fleece-hoodie?variant=12345678"
}
},
{
"@type": "Product",
"name": "Apex Fleece Hoodie — Navy / M",
"sku": "AFH-NAVY-M",
"gtin13": "09501234567891",
"color": "Navy",
"size": "M",
"image": "https://cdn.shopify.com/products/apex-hoodie-navy-m.jpg",
"offers": {
"@type": "Offer",
"price": "79.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"url": "https://store.com/products/apex-fleece-hoodie?variant=12345679"
}
},
{
"@type": "Product",
"name": "Apex Fleece Hoodie — Slate / XL",
"sku": "AFH-SLATE-XL",
"gtin13": "09501234567892",
"color": "Slate",
"size": "XL",
"image": "https://cdn.shopify.com/products/apex-hoodie-slate-xl.jpg",
"offers": {
"@type": "Offer",
"price": "79.00",
"priceCurrency": "USD",
"availability": "https://schema.org/OutOfStock",
"url": "https://store.com/products/apex-fleece-hoodie?variant=12345680"
}
}
]
}
Shopify Liquid Template Implementation
Generate the ProductGroup dynamically from Shopify's product and variant objects in product.liquid:
{% assign options = product.options_with_values %}
Adding GTINs per variant from metafields
{% comment %} Store GTIN per variant in metafield: variants.metafields.inventory.gtin {% endcomment %}
{% for variant in product.variants %}
{
"@type": "Product",
"sku": {{ variant.sku | json }},
{% if variant.metafields.inventory.gtin %}
"gtin13": {{ variant.metafields.inventory.gtin | json }},
{% endif %}
...
}
{% endfor %}
variesBy: Mapping Shopify Options to Schema.org Properties
| Shopify option name | schema.org variesBy value | Variant property to set |
|---|---|---|
| Color / Colour | https://schema.org/color |
"color": "Navy" |
| Size | https://schema.org/size |
"size": "M" (or SizeSpecification for clothing) |
| Material / Fabric | https://schema.org/material |
"material": "Leather" |
| Pattern / Print | https://schema.org/pattern |
"pattern": "Floral" |
| Style / Edition | https://schema.org/additionalProperty |
PropertyValue with name: "Style", value: "Vintage" |
| Storage / Capacity | https://schema.org/additionalProperty |
PropertyValue with unitCode for storage (e.g. GB) |
ProductGroup vs. Separate Product Pages per Variant
Some large catalogs give each variant its own Shopify product (separate handles). In that case, use a single Product block per page (the standard approach) but link variants together using isVariantOf pointing at the parent ProductGroup URL, and isSimilarTo linking to sibling variants. This allows AI agents to traverse the variant graph even when variants are separate URLs.
{% comment %} On a variant-as-separate-product page {% endcomment %}
{
"@type": "Product",
"name": "Apex Hoodie — Navy S",
"isVariantOf": {
"@type": "ProductGroup",
"name": "Apex Hoodie",
"url": "https://store.com/collections/hoodies/apex-hoodie"
}
}
CatalogScan Checks for ProductGroup Coverage
CatalogScan's AI Readiness scan checks whether products with multiple variants use ProductGroup with hasVariant, and whether variesBy correctly maps to Shopify's option names. For apparel and multi-variant stores, this is one of the top-5 structured data gaps found in the scan — the impact on AI agent variant-specific query coverage is significant. The scan also verifies that individual variant Product objects each carry their own availability and variant-specific url.
Related guides: Shopify product variant SEO · Clothing & apparel size schema · Product specifications schema · GTIN & barcode guide
FAQ
What is ProductGroup in schema.org and when should I use it for Shopify?
ProductGroup represents a product with multiple variants (colors, sizes, materials) as a unified entity. It contains a hasVariant array of individual Product objects and a variesBy property. Use it on Shopify product pages with more than one variant to let AI agents navigate your complete variant set in a single structured data pass.
What is the difference between variesBy values in ProductGroup?
variesBy accepts schema.org property URIs matching your Shopify option names: schema.org/color for color options, schema.org/size for size, schema.org/material for material, schema.org/pattern for pattern. These tell AI agents which properties differ across variants and which are shared (name, brand, category).
Does Shopify output ProductGroup JSON-LD by default?
No. Shopify's built-in JSON-LD outputs a single Product with an Offers array or AggregateOffer — not a ProductGroup with individual variant Products. Implementing ProductGroup is a manual template extension that makes each variant individually discoverable by AI agents.
How do I output ProductGroup JSON-LD for Shopify variants in Liquid?
In product.liquid, output a ProductGroup with a hasVariant array built by looping over product.variants. Each variant entry gets its own name, sku, image, color/size properties, and an Offer with variant-specific availability and url (using ?variant={{ variant.id }}).
Should I output all variants in ProductGroup even if some are out of stock?
Yes. Include all variants and set availability to schema.org/OutOfStock for unavailable ones. This lets AI agents answer "does this come in blue?" even when blue is temporarily sold out. Hiding out-of-stock variants misleads agents about your product range.