October 23, 2026 · Muhammad Umar · More by Muhammad Umar
Google's Sale-Duration Properties: Where They Actually Belong
validFrom and validThrough belong on different nodes depending on where your sale price actually lives — here's Google's exact placement rule.

Two Properties, Two Possible Homes
Google's Search Central documentation for merchant listing structured data (Product, Offer) spells out exactly how to mark up the period a sale price is active, using two schema.org properties: validFrom for the start date and time, and either validThrough or priceValidUntil for the end, both in ISO 8601 format (Google's own example: 2025-12-31T23:59:59+01:00). The documentation's best practices are specific: provide both a start and end date/time, make sure the start is earlier than or equal to the end, and include the time and timezone for accuracy in Google's systems.
What's easy to miss is that these properties don't have one fixed location. Where they belong depends on where your sale price actually lives in the markup — and the two options aren't interchangeable.
Rule 1: On the Offer Node
If the price property on your Offer node itself represents the current active sale price, validFrom and validThrough (or priceValidUntil) go directly on that Offer node. This is the simpler, more common case: one price, one node, dates attached where the price lives.
Rule 2: On a PriceSpecification Node
Sale pricing is often modeled differently — with the sale price inside a PriceSpecification node that carries no priceType property, alongside a separate node typed StrikethroughPrice holding the original, higher price. When that's the structure, the duration properties move with the price: validFrom and validThrough go on that specific PriceSpecification node, not on the Offer node above it. There's a hard asymmetry worth flagging here: priceValidUntil is not a valid property on PriceSpecification at all. If a sale price lives on a PriceSpecification node, priceValidUntil simply isn't an option for marking its end date — only validThrough is.
Getting this placement wrong is the kind of mistake that doesn't throw an obvious error. The markup can look complete and well-formed while the sale-duration signal sits on the wrong node, disconnected from the price it's supposed to describe.
A Related Fix: category Now Accepts Custom and Google Taxonomy Values Together
The same documentation clarifies a separate, related property: category on Product now explicitly accepts an array mixing plain text and CategoryCode objects. Plain text strings work the way a custom product_type feed attribute does — Google recommends keeping these under 750 characters. A CategoryCode object instead points at Google's own Product Taxonomy: set @type to CategoryCode, inCodeSet to the taxonomy URL, and codeValue to either a numeric GPC ID (like "2271") or a full path using ">" as the separator between levels (like "Apparel & Accessories > Clothing > Dresses"). A single product can carry several values at once — custom categories and Google-taxonomy codes side by side in the same array.
None of this is presented as a ranking or rich-result-eligibility lever in Google's documentation; it's recommended guidance for giving Search more accurate information about a product, not a requirement. For anyone maintaining structured data at scale, mismatched placement is a quiet, fixable failure worth checking for directly rather than assuming the markup is fine because it validates.