# Model product variants with ProductGroup for AI agents

> Products with sizes, colors or other options should be described as a schema.org ProductGroup with variesBy and hasVariant, where each variant is its own Product with a SKU, price, availability and URL. Without it, AI shopping agents see one product and one price and can't tell whether the size a shopper wants is in stock.

- Source: https://is-agentic-storefront.com/checks/product-variants
- Updated: 2026-10-15
- Category: Product data (25% of score)
- Impact: Medium
- Spec: [Google product variants](https://developers.google.com/search/docs/appearance/structured-data/product-variants)
- Scanner check: Product variants are modeled in structured data

## Why AI agents need variants in structured data

"Is the medium in navy in stock, and how much is it?" is a normal shopping question, and on most product pages the answer lives in a JavaScript variant picker. An agent reading the server HTML sees one price, one availability flag, and a set of swatches it can't interpret.

Google added [product variant structured data](https://developers.google.com/search/docs/appearance/structured-data/product-variants) for this reason: a `ProductGroup` declares which properties vary (`variesBy`) and lists each variant (`hasVariant`) with its own identifier, price, availability and URL. The same data lets an AI shopping agent:

- confirm the exact option combination the shopper asked for exists,
- quote the right price when variants are priced differently,
- check stock per variant instead of per product,
- link straight to the preselected variant.

This complements [variant controls](https://is-agentic-storefront.com/checks/variant-controls), which checks that browser agents can actually _select_ the option on the page.

## What the scanner checks

On up to three sampled product pages, the scanner first decides whether the product has variants. Any of these signals counts:

- a `<select>` whose `name` or `id` mentions option, size, color, variant, style or material, with more than one `<option>`;
- similarly named radio inputs;
- ARIA-stateful option buttons (`aria-pressed`, `role="radio"` with `aria-checked`, `role="option"` with `aria-selected`) inside the product area;
- swatch-style elements (classes like `swatch`, `variant-option`, `color-option`);
- more than one distinct `Offer` SKU or URL in the JSON-LD.

For each product with variants:

| Finding        | Meaning                                                                 |
| -------------- | ----------------------------------------------------------------------- |
| Modeled        | A `ProductGroup` with `hasVariant` or `variesBy`                        |
| Partly modeled | Several `Offer`s or SKUs, but no `ProductGroup`                         |
| Not modeled    | Options on the page, but the structured data describes a single product |

| Result         | Condition                                                                   |
| -------------- | --------------------------------------------------------------------------- |
| Pass           | Every sampled product with variants is modeled.                             |
| Partial        | At least one product is modeled or partly modeled, but not all are modeled. |
| Fail           | No product with variants is modeled or partly modeled.                      |
| Not applicable | None of the sampled products have options (excluded from the score).        |
| Inconclusive   | No product page could be fetched.                                           |

## How to model variants with ProductGroup

```html
<script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "ProductGroup",
    "name": "Merino Crew Sweater",
    "productGroupID": "MCS",
    "brand": { "@type": "Brand", "name": "Your Brand" },
    "variesBy": ["https://schema.org/size", "https://schema.org/color"],
    "hasVariant": [
      {
        "@type": "Product",
        "name": "Merino Crew Sweater, Navy, M",
        "sku": "MCS-NAVY-M",
        "size": "M",
        "color": "Navy",
        "image": "https://your-store.com/images/mcs-navy.jpg",
        "offers": {
          "@type": "Offer",
          "url": "https://your-store.com/products/merino-crew?variant=4421",
          "price": "89.00",
          "priceCurrency": "USD",
          "availability": "https://schema.org/InStock"
        }
      },
      {
        "@type": "Product",
        "name": "Merino Crew Sweater, Navy, L",
        "sku": "MCS-NAVY-L",
        "size": "L",
        "color": "Navy",
        "image": "https://your-store.com/images/mcs-navy.jpg",
        "offers": {
          "@type": "Offer",
          "url": "https://your-store.com/products/merino-crew?variant=4422",
          "price": "89.00",
          "priceCurrency": "USD",
          "availability": "https://schema.org/OutOfStock"
        }
      }
    ]
  }
</script>
```

Steps:

1. **Output a `ProductGroup`** with `productGroupID`, `variesBy` and `hasVariant` for every product that has options.
2. **Make each variant a full `Product`** with its own `sku` or GTIN, its option values (`size`, `color`, `material`, `pattern`) and its own `offers`.
3. **Give each variant a distinct URL** that preselects it (for example `?variant=<id>`) and use it as the offer `url`.
4. **Generate it server-side** from the same variant data the picker uses, so stock and prices stay in sync.

Verify with Google's [Rich Results Test](https://search.google.com/test/rich-results): it should show one `ProductGroup` with one `Product` per variant.

## How to fix it on Medusa

- **Not shipped by default.** The [DTC starter](https://github.com/medusajs/dtc-starter) storefront outputs no Product JSON-LD, so there is no `ProductGroup` either.
- **The data is already there**: Medusa products have [variants](https://docs.medusajs.com/resources/storefront-development/products/variants) with option values and per-variant calculated prices. In the product page server component, map each variant to a `hasVariant` entry with its `sku`, option values, `calculated_price` and stock, and set `variesBy` from the product's options.
- **Pair it with the picker**: the starter's option buttons have no selected state, which the [variant controls](https://is-agentic-storefront.com/checks/variant-controls) check covers. See [Is Medusa agent-ready?](https://is-agentic-storefront.com/guides/medusa-agent-readiness) for both fixes.

## Common mistakes

- One `Product` with an `AggregateOffer` (`lowPrice`/`highPrice`) for a sweater sold in ten sizes. Agents get a price range, not an answer.
- Variants that share one URL, so an agent can't link to the right one.
- `variesBy` values as plain strings like `"Size"` instead of schema.org property URLs.
- Listing only in-stock variants, which hides the "sold out in your size" answer agents need.

## Sources

- [Google: product variant structured data](https://developers.google.com/search/docs/appearance/structured-data/product-variants)
- [schema.org ProductGroup](https://schema.org/ProductGroup)
- [Google: merchant listing structured data](https://developers.google.com/search/docs/appearance/structured-data/merchant-listing)

## Frequently asked questions

### What is a ProductGroup in schema.org?

ProductGroup is the parent of a set of product variants. It lists what the variants differ by (variesBy, for example size and color) and the variants themselves (hasVariant), each a full Product with its own SKU and offer.

### Does this check apply to products without options?

No. If none of the sampled products show size, color or other options, the check is marked not applicable and doesn't affect the score.

### Is a list of Offers with different SKUs enough?

It counts as partial. Agents can see that several SKUs exist but not which option values each one represents. A ProductGroup with variesBy makes that explicit.

## Run your store on a platform built for agentic commerce

Medusa is open-source, headless commerce with a full Store API agents can call and storefronts you control end to end.

### Deploy on Medusa Cloud

Open-source commerce with a full Store API, hosted and scaled for you. Your storefront, your data model.

- [Deploy on Medusa Cloud](https://cloud.medusajs.com?utm_source=agent-friendly-scorer&utm_medium=referral&utm_campaign=markdown-cta&utm_content=%2Fchecks%2Fproduct-variants)
- [Medusa Cloud docs](https://docs.medusajs.com/cloud?utm_source=agent-friendly-scorer&utm_medium=referral&utm_campaign=markdown-cta&utm_content=%2Fchecks%2Fproduct-variants)

### Storefront skills for coding agents

Teach Claude Code, Cursor or Codex to build storefronts with the patterns these checks look for.

```bash
npx skills add medusajs/medusa-agent-skills
```

[View the skill on GitHub](https://github.com/medusajs/medusa-agent-skills/tree/main/plugins/ecommerce-storefront/skills/storefront-best-practices)

---

Scan your store for free at https://is-agentic-storefront.com/