If your product pages were built a few years ago, there's a good chance their structured data is written in Microdata: itemscope, itemtype and itemprop attributes spread through the HTML. It works, but it's tied to the template. Every time someone redesigns a component, the markup can quietly break.
JSON-LD keeps the same information in one <script type="application/ld+json"> block, separate from the layout. Google supports all three formats (JSON-LD, Microdata and RDFa) and says they're equally fine when the markup is valid, but it recommends JSON-LD where your setup allows, as the easiest to implement and maintain at scale and the least prone to errors.
On a marketplace, where listing pages are generated from seller data you don't fully control, that maintainability matters even more. Here's how we're approaching the move.
1. Before: what Microdata looks like on a listing page
A typical Microdata listing mixes the markup into the visible HTML:
<div itemscope itemtype="https://schema.org/Product">
<h1 itemprop="name">Canon EF 50mm f/1.8 STM Lens</h1>
<img itemprop="image" src="/img/listing-123.jpg" alt="Canon EF 50mm lens">
<div itemprop="offers" itemscope itemtype="https://schema.org/Offer">
<span itemprop="priceCurrency" content="USD">$</span>
<span itemprop="price" content="89.00">89.00</span>
<link itemprop="availability" href="https://schema.org/InStock">
<link itemprop="itemCondition" href="https://schema.org/NewCondition">
</div>
</div>
It's valid, but the structured data now depends on the exact nesting of divs. Move the price into a different component during a redesign, and price can end up outside its Offer without anyone noticing.
2. After: the same data as JSON-LD
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Canon EF 50mm f/1.8 STM Lens",
"image": ["https://www.example.com/img/listing-123.jpg"],
"description": "Canon EF 50mm f/1.8 STM lens, new in box.",
"brand": { "@type": "Brand", "name": "Canon" },
"offers": {
"@type": "Offer",
"url": "https://www.example.com/listing/123",
"price": "89.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition",
"seller": { "@type": "Person", "name": "example_seller" }
}
}
</script>
The page's HTML can now change freely, as long as the JSON-LD still describes what the buyer sees.
3. Generate it on the server from your listing model
Hand-writing JSON inside a template is how bugs creep back in. Build it from the same listing object that renders the page, and serialize it with a real JSON library.
![Five steps to generate JSON-LD for a listing page: listing model, map values, serialize, escape, embed and test]
In a Java stack, for example, with Jackson:
Map<String, Object> offer = new LinkedHashMap<>();
offer.put("@type", "Offer");
offer.put("url", listing.getCanonicalUrl());
offer.put("price", listing.getPrice().setScale(2, RoundingMode.HALF_UP).toPlainString());
offer.put("priceCurrency", "USD");
offer.put("availability", listing.isSold()
? "https://schema.org/SoldOut"
: "https://schema.org/InStock");
offer.put("itemCondition", conditionUrl(listing.getCondition()));
offer.put("seller", Map.of("@type", "Person", "name", listing.getSellerName()));
Map<String, Object> product = new LinkedHashMap<>();
product.put("@context", "https://schema.org");
product.put("@type", "Product");
product.put("name", listing.getTitle());
product.put("image", listing.getImageUrls());
product.put("description", listing.getShortDescription());
product.put("offers", offer);
String json = objectMapper.writeValueAsString(product);
Two details in that snippet are there on purpose:
-
Price as a plain decimal string, with no currency symbol or thousands separator, and the currency in
priceCurrencyas an ISO code such asUSD. -
Availability follows the listing's real state. On a marketplace, listings sell out constantly. Marking a sold item
InStocksends buyers to a dead end and breaks the rule that markup must match the page.
4. Escape seller-written text before it goes into a script tag
This one is specific to marketplaces and other user-generated content. Listing titles and descriptions are written by sellers. If a title ever contains </script>, unescaped JSON-LD can close your script tag early, which breaks the page at best and opens an injection hole at worst.
A JSON library escapes quotes and backslashes, but it doesn't make text safe inside HTML. Escape <, > and & as Unicode escapes after serializing:
String safeJson = json
.replace("<", "\\u003c")
.replace(">", "\\u003e")
.replace("&", "\\u0026");
The output is still valid JSON, and parsers read it exactly as before.
5. Map conditions explicitly
Sellers describe condition in their own words; schema.org has a fixed list. Use an explicit mapping rather than passing seller text through:
static String conditionUrl(Condition c) {
switch (c) {
case NEW: return "https://schema.org/NewCondition";
case REFURBISHED: return "https://schema.org/RefurbishedCondition";
case USED: return "https://schema.org/UsedCondition";
default: return null; // omit rather than guess
}
}
If you can't map a value confidently, leave the property out. A wrong condition is worse than a missing one.
6. Name the actual seller, and check what you're eligible for
On a marketplace, the person selling the item usually isn't the marketplace itself. Schema.org's Offer has a seller property, so the markup can say who the seller is.
This also matters for what Google can show. Google's merchant listing documentation says the "merchant has to be the seller of the product in order to be eligible for merchant listing experiences." Read that section carefully against your own setup before expecting merchant listing features. Product markup is still useful for describing the page accurately, but don't assume every rich result applies to a marketplace listing.
7. Remove the Microdata when the JSON-LD goes live
Running both formats for the same product on one page makes it easy for the two to disagree, especially on price and availability. Switch one template at a time, and remove the old itemprop attributes in the same release.
8. Validate before and after
- Rich Results Test on a sample of listing URLs, including sold, used and multi-image listings, not just the tidy ones.
- URL Inspection in Search Console to see the page as Google renders it.
- Search Console's structured data reports after release, to catch errors that only show up across thousands of pages.
Keep in mind that valid structured data makes a page eligible for rich results; it doesn't guarantee them.
Checklist
- Generate JSON-LD on the server from the listing model, with a JSON library.
- Price as a plain decimal string, currency as an ISO code.
- Availability and condition reflect the real listing; omit values you can't map.
- Escape
<,>and&in the serialized JSON. - Use
sellerfor the actual seller, and check merchant listing eligibility rules. - Remove the old Microdata in the same release.
- Test sold, used and edge-case listings, then watch Search Console.
Moving to JSON-LD isn't a ranking trick. It's a way to describe every listing accurately and keep it accurate as the site changes, which matters more the more listings and sellers you have.
FAQ
Is JSON-LD better than Microdata for SEO?
Google treats all three formats as equally valid when the markup is correct. It recommends JSON-LD because it's easier to implement and maintain at scale and less prone to errors.
Can I use JSON-LD and Microdata on the same page?
You can, but describing the same product twice makes it easy for the two to disagree. When migrating, remove the Microdata in the same release that adds the JSON-LD.
What should availability be when a listing has sold?
Use a value that matches the page, such as https://schema.org/SoldOut, rather than leaving InStock in place.
Disclosure: written by the TrueGether team. TrueGether is one of the eBay alternatives built for independent sellers, with listing pages across categories from electronics to collectibles.
Top comments (0)