ProductGroup
ProductGroup is a Product subtype for products that come in variants: different sizes, colors, materials, or configurations of the same item. It has 3 properties of its own: hasVariant (the individual Product variants), variesBy (which dimensions differ), and productGroupID (a group-level identifier). Google reads ProductGroup for the Product snippet and uses the variant structure to show size/color selectors in shopping results.
The type hierarchy is Thing → Product → ProductGroup. Properties like name, brand, and description go on the ProductGroup (shared across all variants). Properties like sku, color, size, and offers go on each individual variant Product inside hasVariant. This avoids duplicating shared data across every SKU.
Full example of schema.org/ProductGroup json-ld markup
The markup is verified as valid with Rich Results Test from Google.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@id": "https://xoocode.com/shop/xootee-classic#group",
"@type": "ProductGroup",
"name": "XooTee Classic",
"description": "Premium cotton t-shirt printed with a stylized rendering of Jane Xoo's 1945 handwritten pediatric medical notes. Available in three sizes and two colors.",
"url": "https://xoocode.com/shop/xootee-classic",
"image": "https://xoocode.com/shop/xootee/xootee-classic-front.jpg",
"brand": { "@id": "https://xoocode.com#organization" },
"productGroupID": "XOOTEE-CLASSIC",
"variesBy": ["size", "color"],
"hasVariant": [
{
"@type": "Product",
"name": "XooTee Classic - Natural Bone, S",
"sku": "XOO-TEE-CL-NB-S",
"color": "Natural Bone",
"size": "S",
"offers": { "@type": "Offer", "price": "495.00", "priceCurrency": "DKK", "availability": "https://schema.org/InStock", "itemCondition": "https://schema.org/NewCondition", "url": "https://xoocode.com/shop/xootee-classic?color=natural-bone&size=s" }
},
{
"@type": "Product",
"name": "XooTee Classic - Natural Bone, M",
"sku": "XOO-TEE-CL-NB-M",
"color": "Natural Bone",
"size": "M",
"offers": { "@type": "Offer", "price": "495.00", "priceCurrency": "DKK", "availability": "https://schema.org/InStock", "itemCondition": "https://schema.org/NewCondition", "url": "https://xoocode.com/shop/xootee-classic?color=natural-bone&size=m" }
},
{
"@type": "Product",
"name": "XooTee Classic - Natural Bone, L",
"sku": "XOO-TEE-CL-NB-L",
"color": "Natural Bone",
"size": "L",
"offers": { "@type": "Offer", "price": "495.00", "priceCurrency": "DKK", "availability": "https://schema.org/OutOfStock", "itemCondition": "https://schema.org/NewCondition", "url": "https://xoocode.com/shop/xootee-classic?color=natural-bone&size=l" }
},
{
"@type": "Product",
"name": "XooTee Classic - Midnight Blue, M",
"sku": "XOO-TEE-CL-MB-M",
"color": "Midnight Blue",
"size": "M",
"offers": { "@type": "Offer", "price": "525.00", "priceCurrency": "DKK", "availability": "https://schema.org/InStock", "itemCondition": "https://schema.org/NewCondition", "url": "https://xoocode.com/shop/xootee-classic?color=midnight-blue&size=m" }
}
]
}
</script>hasVariant
hasVariant takes an array of Product objects, one per variant. Each variant should have its own sku, color or size, and offers (since price and availability can differ by variant). The variants inherit properties from the parent ProductGroup: if the group has a brand, each variant implicitly has that brand. Only set a property on a variant if it differs from the group.
variesBy
variesBy declares which properties differ across variants. It takes text strings or DefinedTerm objects naming the varying dimensions: "color", "size", "material". You can use schema.org property names as short strings. Google reads this to build the variant selector in shopping results. If a product varies by size and color, set "variesBy": ["size", "color"].
productGroupID
productGroupID is a text identifier for the group. This is separate from the individual variant SKUs. It is the parent-level ID that aggregates all variants: the "style number" or "model number" that stays the same across sizes. Not every retailer has a group ID, but if your product database uses one, include it.
ProductGroup vs ProductModel
ProductGroup is for variant sets that exist simultaneously (the same t-shirt in 5 sizes). ProductModel is for a prototypical description of a product (the spec sheet). A ProductModel can be part of a ProductGroup via isVariantOf. For ecommerce product pages with a size/color picker, use ProductGroup with Product variants. For manufacturer catalog pages describing a model, use ProductModel.
Minimal valid version
The smallest markup that still produces a valid ProductGroup entity. Use it as the floor. Reach for the advanced example above when you want search engines and AI agents to understand more about your content.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ProductGroup",
"name": "XooTee Classic",
"productGroupID": "XOOTEE-CLASSIC",
"variesBy": ["size", "color"],
"hasVariant": [
{ "@type": "Product", "name": "XooTee Classic - Natural Bone, M", "sku": "XOO-TEE-CL-NB-M", "color": "Natural Bone", "size": "M", "offers": { "@type": "Offer", "price": "495.00", "priceCurrency": "DKK", "availability": "https://schema.org/InStock" } }
],
"image": "https://xoocode.com/shop/xootee/xootee-classic-front.jpg",
"description": "Premium cotton t-shirt in multiple sizes and colors."
}
</script>Google rich results this unlocks
Markup matching this example makes your page eligible for the following Google Search rich results. The primary target drives the required / recommended property classification in the advanced code block above.
- Google docsProduct snippetprimary
Common ProductGroup mistakes
Mistakes that pass validation but silently fail to earn rich results or mislead consumers walking the graph. Avoid these and your markup will be ahead of most sites in the wild.
- 01
Duplicating shared properties on every variant
WrongEach variant Product repeats brand, description, and image from the groupRightSet shared properties on the ProductGroup; variants inherit themhasVariant Products inherit unset properties from the parent ProductGroup. Duplicating brand, description, and image on every variant bloats the JSON-LD and creates inconsistency risk. Set it once on the group.
- 02
Missing variesBy
WrongProductGroup with hasVariant but no variesByRight"variesBy": ["size", "color"]variesBy tells Google which dimensions differ across variants. Without it, Google has to infer from the variant data. Explicit variesBy lets Google build the correct variant selector (size dropdown, color swatches) in shopping results.
- 03
Using separate Product entities instead of ProductGroup
WrongThree separate Product JSON-LD blocks for the same shirt in S, M, LRightOne ProductGroup with three hasVariant ProductsWithout ProductGroup, Google sees three unrelated products. With it, Google knows these are variants of one item and can show a single listing with a size selector. This consolidates reviews and ratings across variants.
- 04
Missing offers on individual variants
WrongOffers on the ProductGroup but not on individual hasVariant ProductsRightEach variant has its own offers with price and availabilityPrice and availability can differ by variant (a rare size might be out of stock, a premium color might cost more). Always put offers on the variant, not just the group. Google reads the variant-level offers for pricing in search results.
Schema properties in this example
About the example data
The XooTee Classic comes in three sizes and two colors. The ProductGroup holds shared properties (brand, description, image), and each hasVariant Product has its own SKU, size, color, and price. This is the variant-aware version of the standalone Product example.
Comments
Loading comments...