{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/schemas/menu/1.1.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" },
        "available": {
          "description": "False marks the whole menu as temporarily unavailable (86'd) while keeping it in the document.",
          "type": "boolean",
          "default": true
        },
        "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" },
        "available": {
          "description": "False marks the whole section as temporarily unavailable (86'd) while keeping it in the document.",
          "type": "boolean",
          "default": true
        },
        "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 },
        "available": {
          "description": "False marks this variant as temporarily unavailable (86'd) while keeping it in the document.",
          "type": "boolean",
          "default": true
        },
        "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 },
        "available": {
          "description": "False marks this option as temporarily unavailable (86'd) while keeping it in the document.",
          "type": "boolean",
          "default": true
        },
        "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-" }
    }
  }
}
