# Token-efficient product JSON-LD patterns > Lead product JSON-LD with the fields an agent can match and compare: identifiers (gtin, sku, mpn), price, availability, shipping, and returns. Use ProductGroup with hasVariant to keep multi-variant markup compact. This blueprint ships two verified Product JSON-LD skeletons you can copy, a single product with shipping and returns and a multi-variant ProductGroup, and it explains which fields an AI shopping agent actually reads to select you. ## Which fields carry decision weight An agent does not read Product JSON-LD to admire your copy. It reads it to answer a shopper's query and to line you up against competing offers, so the fields worth optimizing are the ones it can match and compare. Google's merchant listing documentation defines the Product and Offer core an agent reads: name, image, description, sku, mpn, a GTIN property (gtin13 for a 13-digit code, or the generic gtin), and brand, alongside an offers object carrying url, priceCurrency, price, priceValidUntil, availability, and itemCondition. The availability value is written as a canonical full schema.org URL, for example https://schema.org/InStock, drawn from the ItemAvailability enumeration that also includes OutOfStock, BackOrder, PreOrder, PreSale, SoldOut, LimitedAvailability, and Discontinued. For itemCondition, Google lists NewCondition, RefurbishedCondition, and UsedCondition, again as full schema.org URLs. Those are the field definitions. The ordering judgment is ours. Among these fields, the ones that plausibly carry the most selection weight are the ones an agent can match against a query and compare across stores: the identifiers that resolve the product to a single global identity, the price and its currency, the availability that says whether it can transact now, and the shipping and return terms that break ties between otherwise equal offers. Free-text description and brand voice are the least comparable, so they earn the least token budget. This is the same facts-per-token discipline the handbook argues for on the page itself, applied to the structured layer. See [information density on machine surfaces](/handbook/information-density/) for the underlying case. ## Skeleton A: single product with shipping and returns Copy this block, swap in your real values, and keep the shape. It carries the identifiers, the offer, an offer-level shipping model, and a return policy in one object. ```json { "@context": "https://schema.org/", "@type": "Product", "name": "Example Widget", "image": ["https://example.com/photos/widget-1x1.jpg"], "description": "One-line factual product description.", "sku": "SKU-12345", "mpn": "MPN-98765", "gtin13": "9780201379624", "brand": { "@type": "Brand", "name": "ExampleBrand" }, "offers": { "@type": "Offer", "url": "https://example.com/products/example-widget", "priceCurrency": "USD", "price": 29.99, "priceValidUntil": "2026-12-31", "availability": "https://schema.org/InStock", "itemCondition": "https://schema.org/NewCondition", "shippingDetails": { "@type": "OfferShippingDetails", "shippingRate": { "@type": "MonetaryAmount", "value": 3.49, "currency": "USD" }, "shippingDestination": { "@type": "DefinedRegion", "addressCountry": "US" }, "deliveryTime": { "@type": "ShippingDeliveryTime", "handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 1, "unitCode": "DAY" }, "transitTime": { "@type": "QuantitativeValue", "minValue": 1, "maxValue": 5, "unitCode": "DAY" } } }, "hasMerchantReturnPolicy": { "@type": "MerchantReturnPolicy", "applicableCountry": "US", "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow", "merchantReturnDays": 30, "returnMethod": "https://schema.org/ReturnByMail", "returnFees": "https://schema.org/FreeReturn" } } } ``` The two nested blocks are where most catalogs are thin, and they are exactly the tie-breakers. Google's merchant listing documentation defines the offer-level shipping shape as offers.shippingDetails of type OfferShippingDetails, carrying a shippingRate (a MonetaryAmount with value and currency), a shippingDestination (a DefinedRegion whose addressCountry is an ISO 3166-1 alpha-2 code), and a deliveryTime (ShippingDeliveryTime) that splits into handlingTime and transitTime, each a QuantitativeValue with minValue, maxValue, and unitCode DAY. Google's return policy documentation (last updated 2025-12-10) defines offers.hasMerchantReturnPolicy of type MerchantReturnPolicy, with applicableCountry, a returnPolicyCategory enum (MerchantReturnFiniteReturnWindow, MerchantReturnNotPermitted, or MerchantReturnUnlimitedWindow), returnMethod such as ReturnByMail, and returnFees such as FreeReturn; merchantReturnDays is an integer and is required when the category is a finite window. We lead this skeleton with the identifiers and the offer, then shipping, then returns, because an agent resolves what the product is before it weighs how the product ships and how returns work. JSON-LD key order means nothing to a parser, so this sequence is for the humans who maintain the file; the load-bearing decision is which fields you populate, not their order. The GTIN 9780201379624 in this skeleton is a checksum-valid placeholder (an ISBN used only so the block validates syntactically). Replace it with your product's real, checksum-valid GTIN before shipping. Google validates the GTIN check digit, so an incorrect digit will not pass validation. ## Skeleton B: multi-variant products (ProductGroup) When a product varies by color, size, or material, do not emit one bloated Product per variant with every shared field repeated. Group them. ```json { "@context": "https://schema.org/", "@type": "ProductGroup", "name": "Example T-Shirt", "description": "Cotton t-shirt.", "url": "https://example.com/products/example-tshirt", "brand": { "@type": "Brand", "name": "ExampleBrand" }, "productGroupID": "TSHIRT-001", "variesBy": ["https://schema.org/color", "https://schema.org/size"], "hasVariant": [ { "@type": "Product", "sku": "TSHIRT-001-RED-M", "name": "Example T-Shirt, Red, M", "color": "Red", "size": "M", "inProductGroupWithID": "TSHIRT-001", "offers": { "@type": "Offer", "url": "https://example.com/products/example-tshirt?variant=red-m", "priceCurrency": "USD", "price": 19.99, "availability": "https://schema.org/InStock", "itemCondition": "https://schema.org/NewCondition" } } ] } ``` The `hasVariant` array holds one Product per real variant; add more objects to it for each color and size permutation you sell. Google's product variant documentation (last updated 2026-05-20) documents ProductGroup with a productGroupID, a variesBy array naming the varying attributes (for example schema.org/color and schema.org/size), and a hasVariant array of Product objects, each carrying its varying attribute values, an inProductGroupWithID back-reference, and its own Offer; Google describes nesting variants under hasVariant as the most compact and natural representation. The pattern stays token-efficient because the shared attributes (name, description, brand, url, productGroupID) are stated once on the group, and each entry in hasVariant restates only what actually varies plus its own offer. A catalog with many size and color permutations therefore avoids repeating identical fields on every variant, which is both smaller to parse and easier to keep consistent. These skeletons were built from live-confirmed schema.org and Google properties, but neither has been run through Google's Rich Results Test or validator.schema.org yet: treat them as verified in structure and unvalidated until you check them. Before either block ships to a real store, paste it into both validators, resolve every error and warning, and replace the placeholder GTIN with your product's real code. ## Before you ship - Paste each skeleton into Google's Rich Results Test and into validator.schema.org, and resolve every error and warning before it goes near a product template. - Replace `9780201379624` with the product's real GTIN, and confirm the check digit is correct so validation passes. - Confirm `availability` and `itemCondition` use full schema.org URLs, not bare strings like `InStock`. - For variant products, check that every entry in `hasVariant` carries its own `offers`, its varying attributes, and an `inProductGroupWithID` that matches the group's `productGroupID`. - Diff the facts in the JSON-LD against the visible page. Any price, availability, or policy that appears in one and not the other is an equivalence problem, and the fix is to make them match. ## Related blueprints and chapters These skeletons are the structured layer of a bigger picture. Pair this with [Product schema for AI shopping](/product-schema-for-ai-shopping/) for the full field walkthrough and the eligibility rules, and read [information density on machine surfaces](/handbook/information-density/) for why the fields an agent can match earn the token budget. When you also serve a Markdown copy of the page, keep the same facts in both by following the [product Markdown mirror](/blueprints/product-markdown-mirror/) blueprint, and see the [machine-readability score](/glossary/#machine-readability-score) for how all of this rolls up into one measure of how usable your data is to an agent.