{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://apiv2.blizzfull.com/schema/menu-feed.schema.json",
  "title": "Menu feed",
  "description": "A restaurant's live menu as Blizzfull serves it from `GET /items/getFullMenu`: its categories, their items, each item's sizes and option groups, and what is available right now.\n\nUse it to show the menu and to build orders. An order line names the item's `id`, its size's `id`, and its choices as `optionSelections`, where `optionGroupId` is the option group's `linkageID` on that item and `optionItemId` is the option's `id`.\n\nIt is a different shape from the menu import format (blizzfullMenuSchema.json): this feed uses Blizzfull's IDs, dollar prices as strings, and the restaurant's current availability.\n\nNew fields may be added at any time. Ignore fields you don't recognise.",
  "type": "array",
  "items": { "$ref": "#/$defs/Category" },
  "$defs": {
    "Category": {
      "type": "object",
      "description": "A section of the menu. Items tagged vegan, gluten free, keto or spicy also appear again in the restaurant's matching special category, and items in no category are listed last under `Uncategorized` (id 0).",
      "required": ["id", "name", "items"],
      "properties": {
        "id": { "type": ["string", "integer"], "description": "The category's Blizzfull ID." },
        "name": { "type": "string" },
        "shortName": { "type": "string", "description": "A URL-safe name." },
        "description": { "type": "string" },
        "parent": { "type": ["string", "integer"], "description": "The parent category's ID, or 0 for a top-level category." },
        "position": { "type": ["string", "integer"], "description": "Display order among its siblings; the array is already sorted by it." },
        "specialType": {
          "type": ["string", "null"],
          "description": "Set on the restaurant's automatic dietary categories (`gluten-free`, `vegan`, `keto`, `spicy`), whose items also appear in their own categories. Null otherwise."
        },
        "active": { "type": "boolean", "description": "False when the restaurant has hidden the category." },
        "displayOnly": { "type": "boolean", "description": "True when the category is shown but can't be ordered from." },
        "secret": { "type": "boolean", "description": "True when the category is hidden from the public menu." },
        "isMenu": { "type": "boolean" },
        "availableTimeData": {
          "description": "When the category can be ordered, by weekday from Sunday: seven `[start, end]` pairs in minutes on a clock that starts at 4 AM (240 is 4:00 AM, 1680 is 4:00 AM the next day). Null means whenever the restaurant is open.",
          "oneOf": [
            { "type": "array", "items": { "type": "array", "items": { "type": "integer" } } },
            { "type": "null" }
          ]
        },
        "availableFrom": { "type": ["string", "null"], "description": "First date the category can be ordered, or null." },
        "availableUntil": { "type": ["string", "null"], "description": "Last date the category can be ordered, or null." },
        "media": { "$ref": "#/$defs/Media" },
        "children": {
          "description": "Subcategories, with the same fields. Null when there are none.",
          "oneOf": [{ "type": "array", "items": { "$ref": "#/$defs/Category" } }, { "type": "null" }]
        },
        "items": { "type": "array", "items": { "$ref": "#/$defs/Item" } }
      }
    },
    "Item": {
      "type": "object",
      "description": "Something a customer can order.",
      "required": ["id", "name", "price", "inactive", "fooditems"],
      "properties": {
        "id": { "type": "integer", "description": "The item's Blizzfull ID: an order line's `id.internal`." },
        "name": { "type": "string" },
        "description": { "type": "string" },
        "category": { "type": "string", "description": "The ID of the item's home category." },
        "categoryName": { "type": "string" },
        "categoryShortName": { "type": "string" },
        "price": {
          "type": "string",
          "description": "The price in dollars, as a decimal string (`\"13.99\"`). For a sized item, its cheapest size.",
          "examples": ["13.99"]
        },
        "inactive": {
          "description": "Whether the item can be ordered now. `false`: available. `\"86\"`: sold out (until `inactiveEnds`, when set). `\"nonexistent\"`: removed from the menu. It is never `true`.",
          "oneOf": [
            { "type": "boolean", "enum": [false] },
            { "type": "string", "enum": ["86", "nonexistent"] }
          ]
        },
        "inactiveEnds": {
          "type": ["string", "null"],
          "description": "When a sold-out item comes back, as Unix seconds in a string. `\"4294967295\"` means until someone turns it back on. Null when available."
        },
        "sizeable": { "type": "boolean", "description": "True when the item comes in sizes." },
        "sizes": {
          "description": "The item's sizes, or null when it has none.",
          "oneOf": [{ "type": "array", "items": { "$ref": "#/$defs/Size" } }, { "type": "null" }]
        },
        "fooditems": {
          "type": "array",
          "description": "The item's parts. Always one entry, which holds the item's option groups.",
          "items": { "$ref": "#/$defs/FoodItem" }
        },
        "options": { "type": "array", "description": "Unused; always empty. Option groups are under `fooditems[].options`." },
        "media": { "$ref": "#/$defs/Media" },
        "spicy": { "type": "boolean" },
        "vegetarian": { "type": "boolean", "description": "The vegan badge." },
        "gf": { "type": "boolean", "description": "Gluten free." },
        "keto": { "type": "boolean" },
        "specialty": { "type": "boolean" },
        "alcoholic": { "type": "boolean", "description": "True when ID must be checked on hand-off." },
        "taxExempt": { "type": "boolean" },
        "orderTypeRestriction": { "type": "string", "description": "When set (`take-out`, `delivery`, `for-here`), the item can only be ordered that way. Empty for any." },
        "appliesToWebApp": { "type": "boolean", "description": "Offered on the website and app." },
        "appliesToKiosk": { "type": "boolean", "description": "Offered at kiosks." },
        "appliesToPos": { "type": "boolean", "description": "Offered at the register." },
        "calorieRange": { "type": "string", "description": "As `min-max`; `0-0` when not set.", "examples": ["500-1200"] },
        "customTags": {
          "type": "array",
          "description": "The restaurant's own labels for the item.",
          "items": {
            "type": "object",
            "properties": {
              "text": { "type": "string" },
              "color": { "type": "string" },
              "bgcolor": { "type": "string" },
              "internalOnly": { "type": "boolean", "description": "True when the label is for staff only: don't show it to customers." }
            }
          }
        },
        "autoAdd": { "type": "boolean" },
        "stock": { "type": "integer" },
        "posData": { "type": ["object", "null"], "description": "Data a point-of-sale integration keeps on the item." }
      }
    },
    "Size": {
      "type": "object",
      "description": "One size of an item.",
      "required": ["id", "name", "cost"],
      "properties": {
        "id": { "type": "string", "description": "The size's Blizzfull ID: an order line's `size.id.internal`." },
        "name": { "type": "string", "examples": ["6 Pcs"] },
        "cost": { "type": "string", "description": "The item's whole price in this size, in dollars, as a decimal string. Not an add-on to `price`.", "examples": ["12.99"] }
      }
    },
    "FoodItem": {
      "type": "object",
      "description": "The item's part that holds its option groups.",
      "properties": {
        "id": { "type": "integer" },
        "name": { "type": "string" },
        "description": { "type": "string" },
        "options": { "type": "array", "items": { "$ref": "#/$defs/OptionGroup" }, "description": "The item's option groups, in display order." }
      }
    },
    "OptionGroup": {
      "type": "object",
      "description": "A set of choices on this item, such as wing flavors.",
      "required": ["id", "title", "type", "items"],
      "properties": {
        "linkageID": {
          "type": "string",
          "description": "This group's link to this item. Send it as `optionGroupId` in an order's `optionSelections`. Unique per item; the same group on another item has a different `linkageID`."
        },
        "id": { "type": "string", "description": "The option group's own ID, shared by every item it is attached to." },
        "componentID": { "type": "string" },
        "title": { "type": "string", "description": "The group's name." },
        "phrase": { "type": "string", "description": "The prompt shown to customers (`Choose your sauce`). May be empty." },
        "kitchenname": { "type": "string", "description": "The name printed for the kitchen. May be empty." },
        "type": {
          "type": "string",
          "enum": ["TYPE_SINGLE_SELECT", "TYPE_MULTI_SELECT", "TYPE_UNIT_STEPPER", "TYPE_VARIABLE_AMOUNT"],
          "description": "How the customer chooses. `TYPE_SINGLE_SELECT`: one option (`selected: 1`). `TYPE_MULTI_SELECT`: any options. `TYPE_UNIT_STEPPER`: a count of each (`selected` is the count). `TYPE_VARIABLE_AMOUNT`: a level of each (`selected` 1 light, 2 regular, 3 extra)."
        },
        "required": { "type": "boolean", "description": "True when the customer must choose." },
        "position": { "type": ["string", "integer"], "description": "Display order; the array is already sorted by it." },
        "defaultIndex": { "type": "integer" },
        "extraProps": {
          "type": ["object", "null"],
          "description": "Limits for this group on this item, when set.",
          "properties": {
            "maxSelectable": { "type": ["integer", "string"], "description": "The most options a customer may choose." },
            "minSelectable": { "type": ["integer", "string"], "description": "The fewest options a customer must choose." },
            "minRequired": { "type": ["integer", "string"] },
            "maxCombinedAmount": { "type": ["integer", "string"], "description": "For a stepper, the most units in total." },
            "amountIncluded": { "type": ["integer", "string"], "description": "How many choices are included free." }
          }
        },
        "sizeEffects": {
          "type": ["object", "null"],
          "description": "Option prices that change with the item's size: `items[optionID][sizeID]` is the option's price in dollars for that size. Null when prices don't change by size."
        },
        "items": { "type": "array", "items": { "$ref": "#/$defs/Option" }, "description": "The options, in display order." }
      }
    },
    "Option": {
      "type": "object",
      "description": "One choice in a group.",
      "required": ["id", "name", "cost", "default"],
      "properties": {
        "id": { "type": ["string", "null"], "description": "The option's Blizzfull ID: send it as `optionItemId`." },
        "name": { "type": ["string", "null"] },
        "cost": {
          "description": "What the option adds, in dollars. For a light/regular/extra option, an object with the price per level: `basePrice` (regular), `light`, and `extra` (dollars, or a percentage of `basePrice` such as `\"200%\"`).",
          "oneOf": [
            { "type": "number" },
            { "type": "string" },
            {
              "type": "object",
              "properties": {
                "basePrice": { "type": "number" },
                "light": { "type": ["number", "string"] },
                "extra": { "type": ["number", "string"] }
              }
            }
          ]
        },
        "default": { "type": "integer", "description": "The option's pre-selected value: 0 when not pre-selected, otherwise the count or level." },
        "unavailableUntil": {
          "type": ["string", "null"],
          "description": "`\"0\"` when available. Otherwise sold out until this Unix time; `\"4294967295\"` means until someone turns it back on."
        },
        "min": { "type": "string", "description": "For a stepper: the fewest of this option, as a decimal string." },
        "max": { "type": "string", "description": "For a stepper: the most of this option, as a decimal string." },
        "position": { "type": "integer" },
        "media": { "type": ["string", "null"] },
        "subOptions": {
          "description": "Groups that open when this option is chosen, with the same fields as a group (no `linkageID`: in `optionSelections`, a nested group is named by its `id`). Null when there are none.",
          "oneOf": [{ "type": "array", "items": { "$ref": "#/$defs/OptionGroup" } }, { "type": "null" }]
        }
      }
    },
    "Media": {
      "description": "Images, or null.",
      "oneOf": [
        {
          "type": "object",
          "properties": {
            "standard": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "id": { "type": "string" },
                  "title": { "type": "string" },
                  "type": { "type": "string", "enum": ["image"] },
                  "source": { "type": "string", "description": "The image URL." }
                }
              }
            }
          }
        },
        { "type": "null" }
      ]
    }
  }
}
