{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/schemas/menu/1.0.0/menu.schema.json",
  "title": "Restaurant Menu Interchange Format",
  "description": "A vendor-neutral interchange format for restaurant and cafe menus. One document describes a single venue and all of its menus (e.g. Breakfast, Lunch, Drinks).",
  "type": "object",
  "required": [
    "schemaVersion",
    "venue",
    "menus"
  ],
  "additionalProperties": false,
  "properties": {
    "schemaVersion": {
      "description": "Version of this interchange format, semver.",
      "type": "string",
      "pattern": "^\\d+\\.\\d+\\.\\d+$"
    },
    "venue": {
      "$ref": "#/$defs/Venue"
    },
    "defaults": {
      "description": "Document-wide defaults that items inherit unless overridden.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "currency": {
          "$ref": "#/$defs/CurrencyCode"
        },
        "language": {
          "$ref": "#/$defs/LanguageTag"
        }
      }
    },
    "modifierGroups": {
      "description": "Reusable option sets (e.g. 'Milk choice', 'Extras') referenced from items by id.",
      "type": "array",
      "items": {
        "$ref": "#/$defs/ModifierGroup"
      }
    },
    "menus": {
      "type": "array",
      "minItems": 1,
      "items": {
        "$ref": "#/$defs/Menu"
      }
    },
    "extensions": {
      "$ref": "#/$defs/Extensions"
    }
  },
  "$defs": {
    "LocalizedText": {
      "description": "Either a plain string (assumed to be in defaults.language) or a map of BCP-47 language tags to strings.",
      "oneOf": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "object",
          "minProperties": 1,
          "propertyNames": {
            "$ref": "#/$defs/LanguageTag"
          },
          "additionalProperties": {
            "type": "string",
            "minLength": 1
          }
        }
      ]
    },
    "LanguageTag": {
      "description": "BCP-47 language tag, e.g. 'en', 'th', 'zh-Hant'.",
      "type": "string",
      "pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$"
    },
    "CurrencyCode": {
      "description": "ISO 4217 alphabetic currency code.",
      "type": "string",
      "pattern": "^[A-Z]{3}$"
    },
    "Id": {
      "description": "Stable identifier, unique within its collection in this document.",
      "type": "string",
      "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$",
      "maxLength": 128
    },
    "Price": {
      "description": "A monetary amount in minor units of the currency (e.g. 450 = USD 4.50, 120 = JPY 120).",
      "type": "object",
      "required": [
        "amount"
      ],
      "additionalProperties": false,
      "properties": {
        "amount": {
          "type": "integer",
          "minimum": 0
        },
        "currency": {
          "$ref": "#/$defs/CurrencyCode",
          "description": "Overrides defaults.currency for this price."
        }
      }
    },
    "PriceDelta": {
      "description": "A signed price adjustment in minor units (negative allowed, e.g. 'no cheese -50').",
      "type": "object",
      "required": [
        "amount"
      ],
      "additionalProperties": false,
      "properties": {
        "amount": {
          "type": "integer"
        },
        "currency": {
          "$ref": "#/$defs/CurrencyCode"
        }
      }
    },
    "Venue": {
      "type": "object",
      "required": [
        "name"
      ],
      "additionalProperties": false,
      "properties": {
        "id": {
          "$ref": "#/$defs/Id"
        },
        "name": {
          "$ref": "#/$defs/LocalizedText"
        },
        "description": {
          "$ref": "#/$defs/LocalizedText"
        },
        "cuisine": {
          "description": "Free-form cuisine labels, e.g. 'thai', 'coffee', 'bakery'.",
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "timezone": {
          "description": "IANA timezone used to interpret availability schedules, e.g. 'Asia/Bangkok'.",
          "type": "string"
        },
        "address": {
          "type": "string"
        },
        "phone": {
          "type": "string"
        },
        "url": {
          "type": "string",
          "format": "uri"
        },
        "logo": {
          "description": "Venue logo image as a base64-encoded string — either a data URI ('data:image/png;base64,…') or raw base64 (assumed PNG). Renderers show it above the venue name; when set, the primary-language name is displayed in the same style as the secondary-language name.",
          "type": "string",
          "minLength": 1
        },
        "taglineLeft": {
          "description": "Single-language tagline shown top-left on every page header.",
          "type": "string",
          "minLength": 1
        },
        "taglineRight": {
          "description": "Single-language tagline shown top-right on every page header.",
          "type": "string",
          "minLength": 1
        },
        "extensions": {
          "$ref": "#/$defs/Extensions"
        }
      }
    },
    "Menu": {
      "description": "A top-level menu such as 'Breakfast', 'All Day', or 'Drinks'.",
      "type": "object",
      "required": [
        "id",
        "name",
        "sections"
      ],
      "additionalProperties": false,
      "properties": {
        "id": {
          "$ref": "#/$defs/Id"
        },
        "name": {
          "$ref": "#/$defs/LocalizedText"
        },
        "description": {
          "$ref": "#/$defs/LocalizedText"
        },
        "availability": {
          "$ref": "#/$defs/Availability"
        },
        "sections": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/$defs/Section"
          }
        },
        "extensions": {
          "$ref": "#/$defs/Extensions"
        }
      }
    },
    "Section": {
      "description": "A named grouping of items. Sections may nest one level or more via 'sections'.",
      "type": "object",
      "required": [
        "id",
        "name"
      ],
      "additionalProperties": false,
      "anyOf": [
        {
          "required": [
            "items"
          ]
        },
        {
          "required": [
            "sections"
          ]
        }
      ],
      "properties": {
        "id": {
          "$ref": "#/$defs/Id"
        },
        "name": {
          "$ref": "#/$defs/LocalizedText"
        },
        "description": {
          "$ref": "#/$defs/LocalizedText"
        },
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/Item"
          }
        },
        "sections": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/Section"
          }
        },
        "extensions": {
          "$ref": "#/$defs/Extensions"
        }
      }
    },
    "Item": {
      "type": "object",
      "required": [
        "id",
        "name"
      ],
      "additionalProperties": false,
      "anyOf": [
        {
          "required": [
            "price"
          ]
        },
        {
          "required": [
            "variants"
          ]
        }
      ],
      "properties": {
        "id": {
          "$ref": "#/$defs/Id"
        },
        "name": {
          "$ref": "#/$defs/LocalizedText"
        },
        "description": {
          "$ref": "#/$defs/LocalizedText"
        },
        "price": {
          "$ref": "#/$defs/Price",
          "description": "Base price. Omit when the item is only sold as variants."
        },
        "variants": {
          "description": "Mutually exclusive purchasable forms of the item (sizes, glass/bottle). If present, these prices replace 'price'.",
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/$defs/Variant"
          }
        },
        "modifierGroupIds": {
          "description": "References into the top-level modifierGroups by id.",
          "type": "array",
          "items": {
            "$ref": "#/$defs/Id"
          }
        },
        "modifierGroups": {
          "description": "Inline modifier groups specific to this item.",
          "type": "array",
          "items": {
            "$ref": "#/$defs/ModifierGroup"
          }
        },
        "dietaryLabels": {
          "type": "array",
          "uniqueItems": true,
          "items": {
            "$ref": "#/$defs/DietaryLabel"
          }
        },
        "allergens": {
          "description": "Allergens the item contains or may contain.",
          "type": "array",
          "uniqueItems": true,
          "items": {
            "$ref": "#/$defs/Allergen"
          }
        },
        "spiceLevel": {
          "description": "0 = not spicy, 5 = extremely spicy.",
          "type": "integer",
          "minimum": 0,
          "maximum": 5
        },
        "nutrition": {
          "$ref": "#/$defs/Nutrition"
        },
        "media": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/Media"
          }
        },
        "tags": {
          "description": "Free-form labels, e.g. 'signature', 'new', 'seasonal'.",
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "available": {
          "description": "False marks the item as temporarily unavailable (86'd) while keeping it in the document.",
          "type": "boolean",
          "default": true
        },
        "availability": {
          "$ref": "#/$defs/Availability",
          "description": "Item-level schedule narrower than its menu's, e.g. lunch-special hours."
        },
        "sku": {
          "description": "External identifier in a POS or ordering system.",
          "type": "string"
        },
        "extensions": {
          "$ref": "#/$defs/Extensions"
        }
      }
    },
    "Variant": {
      "type": "object",
      "required": [
        "id",
        "name",
        "price"
      ],
      "additionalProperties": false,
      "properties": {
        "id": {
          "$ref": "#/$defs/Id"
        },
        "name": {
          "$ref": "#/$defs/LocalizedText"
        },
        "price": {
          "$ref": "#/$defs/Price"
        },
        "sku": {
          "type": "string"
        },
        "default": {
          "type": "boolean",
          "default": false
        },
        "extensions": {
          "$ref": "#/$defs/Extensions"
        }
      }
    },
    "ModifierGroup": {
      "description": "A set of options attached to an item, with selection rules. minSelections > 0 makes the group required.",
      "type": "object",
      "required": [
        "id",
        "name",
        "options"
      ],
      "additionalProperties": false,
      "properties": {
        "id": {
          "$ref": "#/$defs/Id"
        },
        "name": {
          "$ref": "#/$defs/LocalizedText"
        },
        "description": {
          "$ref": "#/$defs/LocalizedText"
        },
        "minSelections": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        },
        "maxSelections": {
          "description": "Omit for unlimited.",
          "type": "integer",
          "minimum": 1
        },
        "options": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/$defs/ModifierOption"
          }
        },
        "extensions": {
          "$ref": "#/$defs/Extensions"
        }
      }
    },
    "ModifierOption": {
      "type": "object",
      "required": [
        "id",
        "name"
      ],
      "additionalProperties": false,
      "properties": {
        "id": {
          "$ref": "#/$defs/Id"
        },
        "name": {
          "$ref": "#/$defs/LocalizedText"
        },
        "priceDelta": {
          "$ref": "#/$defs/PriceDelta",
          "description": "Adjustment applied to the item/variant price. Omit for free options."
        },
        "default": {
          "type": "boolean",
          "default": false
        },
        "allergens": {
          "type": "array",
          "uniqueItems": true,
          "items": {
            "$ref": "#/$defs/Allergen"
          }
        },
        "extensions": {
          "$ref": "#/$defs/Extensions"
        }
      }
    },
    "Availability": {
      "description": "When a menu or item is offered. Interpreted in venue.timezone. Omitted fields mean 'always'.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "schedule": {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/$defs/TimeWindow"
          }
        },
        "startDate": {
          "type": "string",
          "format": "date"
        },
        "endDate": {
          "type": "string",
          "format": "date"
        }
      }
    },
    "TimeWindow": {
      "type": "object",
      "required": [
        "days"
      ],
      "additionalProperties": false,
      "properties": {
        "days": {
          "type": "array",
          "minItems": 1,
          "uniqueItems": true,
          "items": {
            "enum": [
              "mon",
              "tue",
              "wed",
              "thu",
              "fri",
              "sat",
              "sun"
            ]
          }
        },
        "startTime": {
          "description": "Local time HH:MM, inclusive. Omit for start of day.",
          "type": "string",
          "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
        },
        "endTime": {
          "description": "Local time HH:MM, exclusive. May be earlier than startTime to wrap past midnight.",
          "type": "string",
          "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
        }
      }
    },
    "DietaryLabel": {
      "enum": [
        "vegetarian",
        "vegan",
        "pescatarian",
        "gluten-free",
        "dairy-free",
        "nut-free",
        "halal",
        "kosher",
        "organic",
        "low-carb",
        "sugar-free",
        "contains-alcohol",
        "contains-caffeine"
      ]
    },
    "Allergen": {
      "description": "Superset of the EU 14 and US FDA major allergens.",
      "enum": [
        "gluten",
        "wheat",
        "crustaceans",
        "molluscs",
        "fish",
        "eggs",
        "milk",
        "peanuts",
        "tree-nuts",
        "soy",
        "sesame",
        "celery",
        "mustard",
        "lupin",
        "sulphites"
      ]
    },
    "Nutrition": {
      "description": "Per-serving nutrition facts. All optional.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "calories": {
          "type": "number",
          "minimum": 0
        },
        "proteinGrams": {
          "type": "number",
          "minimum": 0
        },
        "carbohydrateGrams": {
          "type": "number",
          "minimum": 0
        },
        "fatGrams": {
          "type": "number",
          "minimum": 0
        },
        "saturatedFatGrams": {
          "type": "number",
          "minimum": 0
        },
        "sugarGrams": {
          "type": "number",
          "minimum": 0
        },
        "fiberGrams": {
          "type": "number",
          "minimum": 0
        },
        "sodiumMilligrams": {
          "type": "number",
          "minimum": 0
        },
        "servingSize": {
          "$ref": "#/$defs/LocalizedText"
        }
      }
    },
    "Media": {
      "type": "object",
      "required": [
        "url"
      ],
      "additionalProperties": false,
      "properties": {
        "url": {
          "type": "string",
          "format": "uri"
        },
        "type": {
          "enum": [
            "image",
            "video"
          ],
          "default": "image"
        },
        "alt": {
          "$ref": "#/$defs/LocalizedText"
        }
      }
    },
    "Extensions": {
      "description": "Namespaced vendor extensions, e.g. {\"x-square\": {...}}. Keys must start with 'x-'.",
      "type": "object",
      "propertyNames": {
        "pattern": "^x-"
      }
    }
  }
}
