JSON Schema for Product Catalogs
Part of the JSON Toolkit{"$ref":"#/$defs/money"}reuses a single price shape everywhere money appears on a product — see below for the complete catalog schema
Explanation
A product catalog has a shape the earlier guides haven't needed yet: the same sub-shape — a price — shows up in more than one place on the same object, and needs to mean exactly the same thing everywhere it appears. Repeating that sub-schema by hand invites drift, where one copy gets updated and another doesn't. $defs plus $ref solves that by defining it once:
{
"$defs": {
"money": {
"type": "object",
"properties": {
"amount": { "type": "integer", "minimum": 0 },
"currency": { "type": "string", "enum": ["USD", "EUR", "GBP"] }
},
"required": ["amount", "currency"],
"additionalProperties": false
}
},
"type": "object",
"properties": {
"sku": { "type": "string", "pattern": "^[A-Z0-9-]+$" },
"name": { "type": "string", "minLength": 1 },
"price": { "$ref": "#/$defs/money" },
"salePrice": { "anyOf": [{ "$ref": "#/$defs/money" }, { "type": "null" }] },
"category": { "type": "string", "enum": ["electronics", "apparel", "home", "toys"] },
"tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
"inStock": { "type": "boolean" }
},
"required": ["sku", "name", "price", "category"],
"additionalProperties": false
}price and salePrice both point at #/$defs/money instead of repeating its properties — change the definition of "money" once (say, adding a supported currency) and every property that references it picks up the change automatically. $ref works as a plain JSON Pointer lookup into the document, so the container is conventionally named $defs (the current spec's recommendation) or definitions (the older Draft-07 name) — either works with real validators, since nothing actually requires the container to be a recognized keyword, only that $ref points at it correctly.
amount is an integer, not a number — storing a price as 19.99 invites the classic floating-point rounding problem (0.1 + 0.2 isn't exactly 0.3 in IEEE 754 arithmetic), so real payment APIs like Stripe's represent money as an integer count of the smallest currency unit (cents) instead — 1999 meaning $19.99, paired with a separate currency field. It's a schema that enforces a specific representation decision, not just a type.
salePrice shows how to make a $ref'd shape nullable: wrap it in anyOf alongside {"type": "null"}, rather than adding "type": ["object", "null"] as a sibling of $ref directly. The Draft-07 spec is explicit that "all other properties in a $ref object MUST be ignored," so a sibling keyword next to $ref is spec-non-compliant behavior to depend on — 2019-09 and later relaxed this and merge siblings normally, and some validators (including this site's) are lenient about it even in Draft-07 mode, but anyOf is the form that's correct under every draft and every validator, not just the lenient ones.
For the vocabulary behind enum and uniqueItems, see What Is JSON Schema?. For $ref and recursive-schema mechanics in more depth, see JSON Schema Validation Explained. For a request-body schema built around an array of nested objects instead, see JSON Schema for API Requests.
Valid examples
{"sku":"SKU-100","name":"Wireless Mouse","price":{"amount":2999,"currency":"USD"},"salePrice":{"amount":2499,"currency":"USD"},"category":"electronics","tags":["wireless","mouse"],"inStock":true}Both price and salePrice reference the same money shape and both satisfy it.
{"sku":"SKU-101","name":"Desk Lamp","price":{"amount":4500,"currency":"USD"},"salePrice":null,"category":"home"}salePrice explicitly set to null — no sale price right now, satisfying the anyOf's null branch.
{"sku":"SKU-102","name":"T-Shirt","price":{"amount":1999,"currency":"EUR"},"category":"apparel"}salePrice and tags both left out entirely — neither is in required, so the object is still valid without them.
Invalid examples
{"sku":"SKU-103","name":"Bad Price","price":{"amount":19.99,"currency":"USD"},"category":"toys"}amount as a decimal — fails the money definition's integer type, which expects a whole-number count of cents, not dollars.
{"sku":"SKU-106","name":"Weird Category","price":{"amount":1000,"currency":"USD"},"category":"furniture"}"furniture" isn't in the category enum — fails even though it's a perfectly reasonable string.
{"sku":"SKU-105","name":"Dup Tags","price":{"amount":1000,"currency":"USD"},"category":"toys","tags":["sale","sale"]}The same tag listed twice — fails uniqueItems, catching a likely copy-paste duplicate.