If you sell products that come in different versions — say, a T-shirt in multiple sizes and colours or a phone with different storage capacities — structured data can help search engines understand those variations clearly.
Without proper schema markup, Google may treat each variant as a completely separate product or fail to understand that they belong to the same parent item.
By implementing product variant schema, you can make your product relationships explicit, keep your listings cleaner, and improve eligibility for rich results.
This guide explains which schema types to use for product variants, how to structure your JSON-LD, and how to manage SKUs and scale your implementation effectively.
Which schema to use for product variants?
To mark up products that have multiple variations (such as size, colour, or material), the best approach is to use ProductGroup and Product schema types together.
- ProductGroup represents the parent product — the umbrella item that all variants belong to.
- Product represents each individual variant (for example, “T-shirt – Medium / Blue”).
- These two types are connected using the properties hasVariant (from the group to each product) and isVariantOf (from each product back to the group).
This relationship allows search engines to understand that the variants share common attributes (like brand or model name) but differ in specific ways, which is defined by the variesBy property.

Here’s how it typically works:
- The parent ProductGroup contains shared information such as name, description, brand, and a list of variant attributes (for example variesBy: [“size”, “color”]).
- Each Product node represents one combination of those attributes, with its own sku, price, availability, and unique URL.
- Together, they form a structured hierarchy that accurately mirrors what users see on your product page.
Using the ProductGroup + Product structure ensures your product data stays consistent, machine-readable, and eligible for richer search displays — especially as Google expands support for product variant markup.
How to implement Schema.org markup for products with multiple variants
Before you start creating the schema, ask yourself: Do all variants live on the same product page (e.g., colour/size selectors change on one URL)? Or does each variant have its own URL (e.g., separate pages for each size/colour)?
According to Google’s “Product variant structured data” document, you can implement either approach (single page with dynamic selectors or multi-page for each variant) as long as the structured data reflects the variants.
Scenario 1: Single product page with selectable variants
Google’s guidance allows single-page setups as long as each variant is properly represented in the structured data.
In this setup, all product variants — for example, different sizes or colours — are available on the same URL. Shoppers can switch between options using dropdowns or buttons, and the page updates dynamically without a full reload.
This is common for modern e-commerce sites built on frameworks like Shopify, WooCommerce, or custom React/Next.js storefronts.
From a structured data perspective, you still need to represent each variant as a distinct Product node, even though they share a single URL.
Step 1: Create a ProductGroup for the parent item
Start by defining a single ProductGroup that represents the overarching product. Include all shared details such as name, description, brand, and variesBy attributes.
Example properties:
- @type: “ProductGroup”
productGroupID: “MODEL-123” (your parent or base SKU)variesBy: [“https://schema.org/color”, “https://schema.org/size”]url: the main product URL- audience: The intended audience for the product. For example, a female audience (using an Audience object with suggestedGender)
hasVariant: references to each variant’s @idOfferShippingDetails: Shared shipping options, costs, and delivery regionshasMerchantReturnPolicy: A return policy shared by all variants (for example, return window and conditions)
Step 2: Choose between nested or unnested hasVariant
You can represent your variants in two ways:
- Nested: Each variant is fully defined inside the hasVariant array within the ProductGroup.
- Unnested: Each variant is defined as its own standalone Product. The shared ProductGroup describes how the product varies using variesBy (for example, color or size), while each Product points back to the same parent ProductGroup using isVariantOf and a shared @id.
Both are valid according to Schema.org and Google guideline.

Nesting is my preferred approach because it keeps the structure hierarchical and clearly signals to search engines that the listed variants belong to a single parent product.
In contrast, unnesting flattens that relationship. It can make it harder for parsers and search systems to understand which products are true variants of the same item.
Pro Tip:
In an unnested setup, you can optionally add a hasVariant property to the ProductGroup that references each variant by @id. This is not required in Google documentation, but it can make the relationship between the ProductGroup and its variants more explicit, especially on single-page setups where all variants are shown together.
Step 3: Add multiple Product nodes for each variant
Under the same page, define separate Product entries for each variant (e.g., “Small / Blue”, “Medium / Red”). Each one should include:
- A unique sku
- Variant-specific attributes like color and size
- A unique @id — often the same base URL with query parameters (for example, ?color=Red&size=M)
- Its own offers section (price, currency, availability)
Step 4: Keep markup in sync with UI
If a variant goes out of stock or changes in price, update the relevant Product node dynamically or regenerate your JSON-LD when the page loads.
Scenario 2: Multi-page setup with one variant per URL
In this approach, each product variant lives on its own dedicated URL — for example:
- example.com/product/tshirt-red
- example.com/product/tshirt-blue
Each page represents a single variant, but all variants are connected through the parent ProductGroup. This setup is often used by larger retailers or product feeds integrated with Google Merchant Center.
Step 1: Define which variants get their own pages and make them self-contained
Start by deciding which variation attributes trigger a new page, such as color. Each of these pages should be a real, indexable URL that reloads when the variant changes.
Once that decision is made, treat each variant page as standalone. Do not rely on a “main” product page to hold shared structured data or context. Search engines evaluate each URL independently, so every variant page must include all the information needed to understand the product and its relationships.
Step 2: Decide whether to use nested or unnested variants
Before writing any variant markup, decide how you want to represent the relationship between the ProductGroup and its variants
With a nested approach, each variant Product is fully defined inside the hasVariant array of the ProductGroup. This creates a clear parent-child structure where all variants are directly contained within the main product.
{
"@context": "https://schema.org",
"@type": "ProductGroup",
"@id": "https://www.example.com/coat#productgroup",
"name": "Winter Coat",
"hasVariant": [
{
"@type": "Product",
"@id": "https://www.example.com/coat/lightblue?size=small#product",
"sku": "LB-S",
"color": "Light Blue",
"size": "Small"
}
]
}
With an unnested approach, the ProductGroup lists only the variant @id values in hasVariant. The full Product definitions live elsewhere in the JSON-LD and link back to the same ProductGroup using isVariantOf and the shared @id.
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "ProductGroup",
"@id": "https://www.example.com/coat#productgroup",
"name": "Winter Coat",
"hasVariant": [
{
"@id": "https://www.example.com/coat/lightblue?size=small#product"
}
]
},
{
"@type": "Product",
"@id": "https://www.example.com/coat/lightblue?size=small#product",
"sku": "LB-S",
"color": "Light Blue",
"size": "Small",
"isVariantOf": {
"@id": "https://www.example.com/coat#productgroup"
}
}
]
}
Both approaches are supported by Schema.org and understood by Google.
Step 3: Add the ProductGroup and the displayed variant Product together
On every variant page, include the full ProductGroup definition so all shared attributes are always present. This includes details like the product name, description, brand, productGroupID, variesBy, and audience.
The ProductGroup should define:
- Shared data like name, description, brand, audience and productGroupID
- The variesBy list (size, color, material, etc.)
- A hasVariant list referencing all variant URLs
Alongside the ProductGroup, fully define one Product that represents the variant currently shown on the page. The Product should includea unique @id, SKU, GTIN, variant attributes (such as color and size), and an Offer whose url matches the current page. The Product must link back to the ProductGroup using isVariantOf.
This ensures that search engines can understand all related variants across different URLs.
Rule of thumb: repeat the ProductGroup on every variant page, and fully describe only the variant that page displays.
Step 4: Reference other variants without fully defining them
Other variants that live on separate pages can be referenced lightly rather than fully described. This keeps structured data aligned with visible content while still helping search engines discover the rest of the product family.
These lightweight references can be URLs or @id values. They signal that other variants exist without implying that those variants are available on the current page.
Step 5: Review canonicals, availability, and consistency
Before publishing—and as part of ongoing maintenance—review the signals that most commonly cause issues in multi-page setups.
Each variant page should use a canonical URL that matches how variants are represented. Pages that truly represent a distinct variant should usually be self-canonical, while secondary variations (such as size) may canonicalize to a primary variant page (such as color).
Availability must always match the current variant’s state. If a specific variant goes out of stock, its structured data should be updated immediately. Mismatches between schema and visible content can cause markup to be ignored.
Finally, confirm consistency across pages: ProductGroup IDs should match, isVariantOf links should be correct, variant identifiers must be unique, and URLs in structured data should align with the actual page URL.
Product Variant JSON-LD Example
Scenario 1: Single page (all variants selectable without reloads)
In a single-page variant setup, one URL represents the entire product family and displays all selectable variants on the same page.
Each variant can be fully described on the same page, either nested inside hasVariant or defined as separate Product nodes. They are linked to the shared ProductGroup using IDs, allowing search engines to understand that all variants belong to the same product family and are presented together on a single page.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "ProductGroup",
"@id": "https://www.example.com/product/tee-101#group",
"name": "Classic Cotton Tee",
"description": "Soft, breathable T-shirt available in multiple sizes and colours.",
"brand": { "@type": "Brand", "name": "ExampleWear" },
"url": "https://www.example.com/product/tee-101",
"image": [
"https://www.example.com/images/tee-black.jpg",
"https://www.example.com/images/tee-blue.jpg"
],
"productGroupID": "TEE-101",
"variesBy": [
"https://schema.org/color",
"https://schema.org/size"
],
"hasVariant": [
{ "@type": "Product", "@id": "https://www.example.com/product/tee-101?color=Black&size=S" },
{ "@type": "Product", "@id": "https://www.example.com/product/tee-101?color=Blue&size=M" }
]
},
{
"@type": "Product",
"@id": "https://www.example.com/product/tee-101?color=Black&size=S",
"name": "Classic Cotton Tee — Black / Small",
"isVariantOf": { "@id": "https://www.example.com/product/tee-101#group" },
"color": "Black",
"size": "S",
"sku": "TEE-101-BLK-S",
"gtin13": "0123456789012",
"image": "https://www.example.com/images/tee-black-s.jpg",
"offers": {
"@type": "Offer",
"url": "https://www.example.com/product/tee-101?color=Black&size=S",
"priceCurrency": "USD",
"price": "19.99",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition",
"seller": { "@type": "Organization", "name": "Example Store" }
}
},
{
"@type": "Product",
"@id": "https://www.example.com/product/tee-101?color=Blue&size=M",
"name": "Classic Cotton Tee — Blue / Medium",
"isVariantOf": { "@id": "https://www.example.com/product/tee-101#group" },
"color": "Blue",
"size": "M",
"sku": "TEE-101-BLU-M",
"gtin13": "0123456789013",
"image": "https://www.example.com/images/tee-blue-m.jpg",
"offers": {
"@type": "Offer",
"url": "https://www.example.com/product/tee-101?color=Blue&size=M",
"priceCurrency": "USD",
"price": "21.99",
"availability": "https://schema.org/OutOfStock",
"itemCondition": "https://schema.org/NewCondition",
"seller": { "@type": "Organization", "name": "Example Store" }
}
}
]
}
</script>
Scenario 2: Multi page (each variant on its own URL)
In a multi-page variant setup, each URL should include the complete ProductGroup so shared attributes (brand, description, variesBy, audience, etc.) are always present on that page.
Other variants can be referenced by URL. Variants that are not fully described on the current page can be listed as lightweight references (for example, { “url”: “…” }), so search engines can discover the rest of the product family.
<script type="application/ld+json">
[
{
"@context": "https://schema.org/",
"@type": "ProductGroup",
"name": "Cotton T-shirt",
"description": "Classic cotton T-shirt for everyday wear",
"brand": {
"@type": "Brand",
"name": "My Nice brand"
},
"productGroupID": "TSHIRT-001",
"variesBy": [
"https://schema.org/size",
"https://schema.org/color"
],
"audience": {
"@type": "PeopleAudience",
"suggestedGender": "http://schema.org/Female "
},
"hasVariant": [
{
"@type": "Product",
"@id": "https://www.example.com/tshirt/white?size=small#product",
"name": "Small white T-shirt",
"description": "Small white cotton T-shirt",
"image": "https://www.example.com/tshirt_small_white.jpg",
"size": "small",
"color": "white",
"offers": {
"@type": "Offer",
"url": "https://www.example.com/tshirt/white?size=small",
"price": 19.99,
"priceCurrency": "USD"
}
},
{
"@type": "Product",
"@id": "https://www.example.com/tshirt/white?size=large#product",
"name": "Large white T-shirt",
"description": "Large white cotton T-shirt",
"image": "https://www.example.com/tshirt_large_white.jpg",
"size": "large",
"color": "white",
"offers": {
"@type": "Offer",
"url": "https://www.example.com/tshirt/white?size=large",
"price": 19.99,
"priceCurrency": "USD"
}
},
{
"@id": "https://www.example.com/tshirt/black?size=medium#product",
"url": "https://www.example.com/tshirt/black?size=medium"
},
{
"@id": "https://www.example.com/tshirt/black?size=large#product",
"url": "https://www.example.com/tshirt/black?size=large"
}
]
}
]
</script>
Common errors when creating Product Variant schema
Defining the ProductGroup only once when using multiple page setup
In a multi-page variant setup, each variant lives on its own URL and is treated as a separate page. For example, different colors may have their own URLs, with sizes handled as parameters or selections on those pages. Users can switch between variants (such as changing color) using the UI, which triggers a full page reload rather than updating content dynamically.
Because each variant page can be accessed directly and evaluated on its own, there is no single “master” page that defines the ProductGroup and is referenced by all other pages. Each page must independently describe the shared product context.
The ProductGroup exists to capture attributes that are common across all variants, such as brand, material, and intended audience. Since this information applies to every variant page, the complete ProductGroup definition must be included and repeated on each variant URL. Referencing a ProductGroup defined on a different page is not sufficient in a multi-page setup.
To avoid this issue, the full ProductGroup definition should be repeated on every variant page. This ensures that each page can stand on its own while still clearly belonging to the same group of variants.
Missing or inconsistent variant identifiers
Variants are differentiated primarily through identifiers such as sku or gtin. Reusing these identifiers across multiple variants is a critical error that breaks the variant relationship.
When identifiers are duplicated, search engines cannot reliably distinguish one variant from another. This can lead to variants being merged incorrectly or treated as a single product with conflicting attributes.
Each variant must have its own unique identifiers, and those identifiers must be used consistently across all references. This includes matching @id values, offer URLs, and any references from hasVariant or isVariantOf.
Not linking variants back to the ProductGroup
Defining Product variants without linking them back to the parent ProductGroup is another common problem. Without this connection, variants may appear as standalone products rather than part of a unified product family.
The isVariantOf property is essential because it explicitly tells search engines which ProductGroup a variant belongs to. Without it, the relationship between variants remains implicit or unclear.
Every variant Product should include an isVariantOf reference pointing to the same ProductGroup @id. This applies whether you are using nested or unnested markup.
Mixing nested and unnested approaches incorrectly
Schema.org allows both nested and unnested variant structures, but mixing them inconsistently can cause confusion. For example, nesting one variant inside hasVariant while defining others separately without clear references weakens the overall structure.
This inconsistency makes it harder for parsers to understand whether Products are siblings, parents, or unrelated entities. In complex setups, this can lead to partial parsing or ignored markup.
Choose one approach per page and apply it consistently. If you use nesting, nest only the variant shown on the page. If you use an unnested approach, ensure all variants are properly linked using @id and isVariantOf.
Using variant attributes at the wrong level
Another common error is placing variant-specific attributes, such as size, color, price, or availability, on the ProductGroup. This can result in conflicting information when variants differ by these attributes.
The ProductGroup should only contain attributes shared across all variants. Variant-specific details belong on individual Product nodes so they accurately reflect what the user sees and can purchase.
Keeping a clean separation between shared and variant-specific attributes improves clarity and reduces the risk of search engines misinterpreting which details apply to which product.
Structured data not matching page content
Structured data must reflect the actual content of the page. When there is a mismatch—such as listing prices, availability, or variants that are not visible—search engines may treat the markup as unreliable.
One of the most common causes of structured data issues is failing to update variant-level data when the product state changes. This often happens when a specific variant goes out of stock, but the structured data still shows it as available or purchasable.
Search engines compare structured data against what users can actually do on the page. If a variant is marked as InStock in schema but the UI clearly shows it as sold out, disabled, or unavailable, that mismatch can cause the structured data to be ignored or flagged as unreliable.
To avoid this, availability (InStock, OutOfStock, PreOrder, etc.) should always be tied to the currently displayed variant, not to the ProductGroup or a default variant. When a variant goes out of stock, its corresponding Product schema and Offer must be updated immediately to reflect the new state.
Keeping structured data in sync with real-time inventory helps maintain trust with search engines and ensures your product pages remain eligible for rich results.
Incorrect or inconsistent canonical URLs on variant pages
When a variant is presented on its own dedicated page, that page should use a self-referencing canonical. This tells search engines that the URL represents a distinct product variant with its own SKU, price, and availability.
However, this does not mean that every possible variant combination must have its own canonical URL. In many setups, only one dimension (such as color) is treated as canonical, while other dimensions (such as size) are handled as secondary variations on the same page.
For example, if each color has its own URL and page, those color pages should be self-canonical. Size variations can then live on the same page and point to the color-level canonical URL, as long as the page content and structured data reflect that setup.
The key rule is consistency: a page should only be canonical if it truly represents a distinct variant in both the UI and the structured data. Canonicals, visible content, and Product schema must all align to describe the same level of variation.
Summary
Implementing product variant schema (via ProductGroup + Product variant markup) is a powerful way to help search engines understand your variant-rich ecommerce offerings, and can help surface more precise product results (for example size/colour combinations) in search.
But like many things in large-scale ecommerce, the key is automation, data quality, alignment with your SKU/inventory systems, and ongoing monitoring.
