If you landed here searching "schema price," you're probably after one of two things: what the price property in schema.org markup actually does, or how much a schema tool costs. This is about the first one — specifically, how to put a price inside Product structured data so Google can use it, and where people get it wrong.
What price actually is in Product schema
In schema.org's vocabulary, price isn't a property of Product directly — it lives on Offer, the object that describes the terms under which something is being sold. A minimal structure looks like this:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Executive Anvil",
"offers": {
"@type": "Offer",
"price": "119.99",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
}
}
price accepts a number or a plain text value, and there's also PriceSpecification, a more detailed structured type with fields like minPrice, maxPrice, and valueAddedTaxIncluded. Most sites never need it — a plain price and priceCurrency on the Offer covers a single fixed price. PriceSpecification earns its keep when you need to say more than "this costs X," which is where ranges and multi-seller listings come in later.
Four fields do the actual work, and only two of them are strictly required. The table below spells out which is which, and what Google does with each one:
| Field | Required by Google? | Notes |
|---|---|---|
price | Yes | Must be the current, active price — and it has to be greater than zero for Merchant listings, a stricter rule than schema.org's own spec. |
priceCurrency | Yes, if price is set | ISO 4217 three-letter code (USD, EUR, GBP), never a currency symbol. |
availability | Recommended | InStock, OutOfStock, PreOrder, LimitedAvailability, BackOrder, Discontinued, InStoreOnly, OnlineOnly, PreSale, or SoldOut — see the full list on Google's product snippet reference. |
priceValidUntil | Recommended | ISO 8601 date. A past date can keep the listing from showing. |
Source: Google's Merchant listing structured data reference, page as of July 2026 — Google updates these requirements periodically, so check the live page before shipping anything that depends on an exact rule.
Why "recommended" doesn't mean optional
availability and priceValidUntil are listed as recommended, not required, which makes it tempting to skip them. In practice, that's a mistake with two different price fields. First, priceValidUntil — Google's own documentation is blunt about it: a listing may not display once that date is in the past. There's no published grace period, no "a few days won't hurt." If you set it and forget it, the listing degrades silently, the same way an expired robots.txt directive quietly stops working instead of throwing an error. A stale priceValidUntil is the structured-data equivalent of a "sale ends Sunday" banner nobody took down: technically still there, doing nothing, fooling no one but the shopper who trusted it.
Watch out
Google checks that price and availability in your structured data match what's actually on the page, and what's in your product feed if you use Merchant Center. A mismatch — say, the schema says "$49.99" but the visible page has updated to "$54.99" — gets flagged as an inaccurate price status, and it's your live page that decides which number is correct.
Second, Google's requirements aren't frozen. In February 2025, Google added a beta validForMemberTier property so merchants can markup a separate loyalty-member price alongside the regular one — a price specification can't carry both that and a priceType value at the same time, and if it does, Google ignores the combination. If you're running a membership or loyalty program, that's worth reading the current spec for rather than guessing, since a beta property like this one is exactly the kind of thing that changes again before you'd notice.
Common mistakes
The table below lists five ways price markup breaks in practice, and the fix for each — all mechanical once you know which field is the culprit:
| Mistake | What happens | Fix |
|---|---|---|
priceCurrency missing | Google can't interpret the number — no price shown | Always pair price with a three-letter ISO 4217 code |
| Currency symbol instead of code | €49.99 or $49.99 fails validation | Use EUR / USD as the currency, 49.99 as the price |
| Price is zero or blank as a placeholder | Merchant listing is rejected outright | Don't ship a price field until you have a real number |
priceValidUntil left in the past | Listing may stop showing, with no warning in Search Console | Set it forward and keep it maintained, or drop it if you can't |
| Schema price doesn't match the visible page | Flagged as inaccurate price status | Generate the markup from the same source of truth as the page copy |
None of these throw a visible error on your page — JSON-LD fails silently. The Schema Validator will catch structural problems like a missing priceCurrency, but a stale priceValidUntil or a mismatch against your visible price is something only you can catch, because the JSON itself is perfectly valid.
Price ranges and multiple sellers
A single Offer with one price only covers a single fixed price from a single seller. Two other situations need different handling:
- A price range for one product: a T-shirt in three sizes at the same price doesn't need a range, but a product genuinely sold "from $80 to $120" depending on configuration uses
PriceSpecificationwithminPriceandmaxPriceinstead of a flatprice. - The same product sold by multiple sellers at different prices: this is what
AggregateOfferis for, withlowPrice,highPrice,offerCount, andpriceCurrencydescribing the spread across sellers, not a single seller's own range.
An AggregateOffer for a product eight different retailers stock might look like this — note the type change from Offer to AggregateOffer, and that there's no single price field at all:
{
"@type": "AggregateOffer",
"lowPrice": "89.00",
"highPrice": "129.99",
"priceCurrency": "USD",
"offerCount": "8"
}
Mixing these up is an easy trap: putting lowPrice/highPrice on a plain Offer instead of switching the type to AggregateOffer produces markup that validates as JSON but doesn't mean what you intended. If you're building the markup by hand, this is exactly the kind of type mismatch that's easy to miss and annoying to debug later — the same category of mistake as the address-as-plain-string error that breaks LocalBusiness markup: the JSON is well-formed, the meaning isn't what the schema expects.
Build it with the generator instead
Our free Product Schema Generator handles the price, currency, availability, and GTIN fields for you, and outputs ready-to-paste JSON-LD for a single-price Offer. It runs entirely in your browser — nothing you type is sent to a server or stored anywhere.
For range and multi-seller AggregateOffer structures, you'll still want to write the JSON-LD by hand or extend the generator's output — that's a deliberate gap in a generic tool, not a bug, since ranges and per-seller data are specific to how your catalog is structured.
