{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://openbook-data-standards.github.io/openbook/schema/common.schema.json",
  "title": "OpenBook common definitions",
  "description": "Field names are camelCase and follow schema.org where a property exists (startDate, dateModified, alternateName, sameAs, identifier, superEvent). Every document MAY carry JSON-LD @context / @type.",
  "$defs": {
    "openbookVersion": {
      "type": "string",
      "maxLength": 64,
      "examples": [
        "0.3.0-draft"
      ]
    },
    "sequence": {
      "type": "integer",
      "minimum": 0,
      "description": "Monotonic per feed, issued by the publisher's server. The `since` cursor."
    },
    "dateTime": {
      "type": "string",
      "format": "date-time",
      "description": "RFC 3339 (ISO 8601 with an explicit offset)."
    },
    "decimalString": {
      "type": "string",
      "pattern": "^-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?$",
      "maxLength": 40,
      "description": "Exact decimal as a string, not a JSON number (Q39)."
    },
    "decimalOdds": {
      "type": [
        "string",
        "null"
      ],
      "pattern": "^(?:1\\.(?:0*[1-9][0-9]*)|[2-9][0-9]*(?:\\.[0-9]+)?|[1-9][0-9]+(?:\\.[0-9]+)?)$",
      "maxLength": 40,
      "description": "Decimal odds as a decimal string. MUST be strictly greater than 1. null = tombstone (Merge Patch removed). Never a sentinel (not \"0\"). American and fractional forms are presentation only."
    },
    "money": {
      "type": "object",
      "required": [
        "amount"
      ],
      "additionalProperties": false,
      "patternProperties": {
        "^x_": {}
      },
      "properties": {
        "amount": {
          "$ref": "#/$defs/decimalString"
        }
      },
      "description": "A monetary value in the feed's baseCurrency (Q44). Incremental messages do not repeat currency. Odds are not money."
    },
    "iso4217": {
      "type": "string",
      "pattern": "^[A-Z]{3}$",
      "description": "ISO 4217 alpha code (GBP, EUR, USD)."
    },
    "sharedId": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9-]*(:[a-z0-9-]+)+$",
      "maxLength": 128,
      "description": "Short form of a shared id (sport:soccer). Formal form: urn:openbook:sport:soccer."
    },
    "ownId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 128,
      "description": "The publisher's own id, unique within the publisher."
    },
    "territory": {
      "type": "string",
      "pattern": "^[A-Z]{2}(-[A-Z0-9]{1,3})?$",
      "description": "CLDR territory code: ISO 3166-1 alpha-2 (GB) or ISO 3166-2 subdivision (GB-ENG, US-PR)."
    },
    "sameAs": {
      "type": [
        "string",
        "null"
      ],
      "pattern": "^https://www\\.wikidata\\.org/entity/Q[0-9]+$",
      "maxLength": 128,
      "description": "schema.org sameAs \u2192 the Wikidata entity URL. The shared entity id: required when one exists, null otherwise."
    },
    "identifier": {
      "type": "array",
      "maxItems": 100,
      "description": "schema.org identifier as PropertyValue list \u2014 external cross-reference ids.",
      "items": {
        "type": "object",
        "required": [
          "propertyID",
          "value"
        ],
        "additionalProperties": false,
        "properties": {
          "propertyID": {
            "type": "string",
            "maxLength": 128,
            "examples": [
              "sportradar",
              "opta"
            ]
          },
          "value": {
            "type": "string",
            "maxLength": 256
          }
        }
      }
    },
    "alternateName": {
      "type": "array",
      "maxItems": 100,
      "items": {
        "type": "string",
        "maxLength": 256
      },
      "description": "schema.org alternateName \u2014 aliases and other spellings."
    },
    "localizedNames": {
      "type": "object",
      "propertyNames": {
        "pattern": "^[a-z]{2}$"
      },
      "additionalProperties": {
        "type": "string",
        "maxLength": 256
      },
      "description": "Per-language names keyed by ISO 639-1."
    },
    "nameLatin": {
      "type": "string",
      "description": "Latin-script form. Method: ISO 9 (Cyrillic) or ISO 843 (Greek)."
    },
    "iso6391": {
      "type": "string",
      "pattern": "^[a-z]{2}$",
      "description": "ISO 639-1 language code (Q68)."
    },
    "timeZone": {
      "type": "string",
      "minLength": 1,
      "description": "IANA time-zone database name (Europe/London). Display only; timestamps stay RFC 3339 with offset (Q9, Q64)."
    },
    "latitude": {
      "type": "number",
      "minimum": -90,
      "maximum": 90,
      "description": "WGS 84 latitude (Q70)."
    },
    "longitude": {
      "type": "number",
      "minimum": -180,
      "maximum": 180,
      "description": "WGS 84 longitude (Q70)."
    },
    "place": {
      "type": "object",
      "additionalProperties": false,
      "description": "schema.org Place (Q62). City + territory; no street or postal (Q69). No capacity (Q71).",
      "properties": {
        "name": {
          "type": "string"
        },
        "shortName": {
          "type": "string"
        },
        "registeredName": {
          "type": "string"
        },
        "alternateName": {
          "$ref": "#/$defs/alternateName"
        },
        "names": {
          "$ref": "#/$defs/localizedNames"
        },
        "nameLatin": {
          "$ref": "#/$defs/nameLatin"
        },
        "addressLocality": {
          "type": "string"
        },
        "territory": {
          "$ref": "#/$defs/territory"
        },
        "timeZone": {
          "$ref": "#/$defs/timeZone"
        },
        "latitude": {
          "$ref": "#/$defs/latitude"
        },
        "longitude": {
          "$ref": "#/$defs/longitude"
        },
        "sameAs": {
          "$ref": "#/$defs/sameAs"
        },
        "identifier": {
          "$ref": "#/$defs/identifier"
        }
      },
      "patternProperties": {
        "^x_": {}
      }
    },
    "surface": {
      "enum": [
        "grass",
        "clay",
        "hard",
        "turf",
        "ice",
        "indoor",
        "other"
      ],
      "description": "This fixture's playing surface (Q84)."
    },
    "gender": {
      "enum": [
        "men",
        "women",
        "mixed",
        "open",
        "other"
      ],
      "description": "Competition gender on the league, not the person (Q72). Vocab, not ISO 5218."
    },
    "ageGroup": {
      "type": "string",
      "minLength": 1,
      "description": "League age grade (open, U21, U19, …). Growable vocab (Q73)."
    },
    "hand": {
      "enum": [
        "left",
        "right",
        "both"
      ],
      "description": "Throwing/shooting or batting side (Q88). No ISO."
    },
    "side": {
      "enum": [
        "home",
        "away",
        "draw",
        "over",
        "under",
        "yes",
        "no",
        "participant",
        "other",
        "odd",
        "even",
        "none",
        "home-or-draw",
        "away-or-draw",
        "home-or-away"
      ],
      "description": "Q105 extra tokens: odd, even, none, home-or-draw, away-or-draw, home-or-away. none is a listed selection (Q116). other is leftover."
    },
    "homeAwayDraw": {
      "enum": [
        "home",
        "away",
        "draw"
      ],
      "description": "HT/FT halfTime and fullTime only (Q109)."
    },
    "propOutcomePairing": {
      "description": "Row rules Q110, Q112, Q114, Q119–Q121, Q126–Q131. Listed CS both totals; HT/FT both halves; plus-band is participant + atLeast, not line; yes/no player has player and not leftover other or outcome line; no mix of boards.",
      "allOf": [
        {
          "if": {
            "required": [
              "halfTime"
            ]
          },
          "then": {
            "required": [
              "fullTime"
            ]
          }
        },
        {
          "if": {
            "required": [
              "fullTime"
            ]
          },
          "then": {
            "required": [
              "halfTime"
            ]
          }
        },
        {
          "if": {
            "required": [
              "homeTotal"
            ]
          },
          "then": {
            "required": [
              "awayTotal"
            ]
          }
        },
        {
          "if": {
            "required": [
              "awayTotal"
            ]
          },
          "then": {
            "required": [
              "homeTotal"
            ]
          }
        },
        {
          "if": {
            "required": [
              "atLeast"
            ]
          },
          "then": {
            "required": [
              "participant"
            ]
          }
        },
        {
          "if": {
            "required": [
              "atLeast"
            ]
          },
          "then": {
            "not": {
              "anyOf": [
                {
                  "required": [
                    "line"
                  ]
                },
                {
                  "required": [
                    "homeTotal"
                  ]
                },
                {
                  "required": [
                    "halfTime"
                  ]
                },
                {
                  "required": [
                    "player"
                  ]
                }
              ]
            }
          }
        },
        {
          "if": {
            "required": [
              "homeTotal"
            ]
          },
          "then": {
            "not": {
              "anyOf": [
                {
                  "required": [
                    "halfTime"
                  ]
                },
                {
                  "required": [
                    "player"
                  ]
                },
                {
                  "required": [
                    "atLeast"
                  ]
                }
              ]
            }
          }
        },
        {
          "if": {
            "required": [
              "halfTime"
            ]
          },
          "then": {
            "not": {
              "anyOf": [
                {
                  "required": [
                    "homeTotal"
                  ]
                },
                {
                  "required": [
                    "player"
                  ]
                },
                {
                  "required": [
                    "atLeast"
                  ]
                }
              ]
            }
          }
        },
        {
          "if": {
            "required": [
              "player"
            ]
          },
          "then": {
            "not": {
              "anyOf": [
                {
                  "required": [
                    "homeTotal"
                  ]
                },
                {
                  "required": [
                    "halfTime"
                  ]
                },
                {
                  "required": [
                    "atLeast"
                  ]
                }
              ]
            }
          }
        },
        {
          "if": {
            "required": [
              "player"
            ]
          },
          "then": {
            "properties": {
              "side": {
                "not": {
                  "const": "other"
                }
              }
            }
          }
        },
        {
          "if": {
            "required": [
              "player"
            ],
            "properties": {
              "side": {
                "enum": [
                  "yes",
                  "no"
                ]
              }
            }
          },
          "then": {
            "not": {
              "required": [
                "line"
              ]
            }
          }
        }
      ]
    },
    "marketOutcome": {
      "type": "object",
      "required": [
        "side"
      ],
      "additionalProperties": false,
      "patternProperties": {
        "^x_": {}
      },
      "properties": {
        "side": {
          "$ref": "#/$defs/side"
        },
        "participant": {
          "$ref": "#/$defs/ownId",
          "description": "Required when side = participant. Listed winning margin always has this plus outcome line (Q113) or atLeast (Q120)."
        },
        "odds": {
          "$ref": "#/$defs/decimalOdds"
        },
        "line": {
          "$ref": "#/$defs/decimalString"
        },
        "active": {
          "type": "boolean"
        },
        "homeTotal": {
          "type": "number",
          "description": "Listed correct-score home amount (Q105). JSON number like scores[].total. Pair with awayTotal (Q112)."
        },
        "awayTotal": {
          "type": "number",
          "description": "Listed correct-score away amount (Q105). Pair with homeTotal (Q112)."
        },
        "halfTime": {
          "$ref": "#/$defs/homeAwayDraw"
        },
        "fullTime": {
          "$ref": "#/$defs/homeAwayDraw"
        },
        "player": {
          "$ref": "#/$defs/ownId",
          "description": "Player over/under and yes/no player rows: OpenBook player id only (Q108, Q115, Q126, Q127)."
        },
        "atLeast": {
          "type": "number",
          "description": "Listed winning-margin plus-band floor (Q119). JSON number like homeTotal. 3 means 3 or more. Pair with participant (Q120). Not outcome line (Q121)."
        }
      },
      "allOf": [
        {
          "$ref": "#/$defs/propOutcomePairing"
        }
      ]
    },
    "gradeOutcome": {
      "type": "object",
      "required": [
        "side",
        "result"
      ],
      "additionalProperties": false,
      "properties": {
        "side": {
          "$ref": "#/$defs/side"
        },
        "participant": {
          "$ref": "#/$defs/ownId"
        },
        "line": {
          "$ref": "#/$defs/decimalString"
        },
        "result": {
          "$ref": "#/$defs/gradeResult"
        },
        "homeTotal": {
          "type": "number"
        },
        "awayTotal": {
          "type": "number"
        },
        "halfTime": {
          "$ref": "#/$defs/homeAwayDraw"
        },
        "fullTime": {
          "$ref": "#/$defs/homeAwayDraw"
        },
        "player": {
          "$ref": "#/$defs/ownId"
        },
        "atLeast": {
          "type": "number",
          "description": "Listed winning-margin plus-band floor (Q119). JSON number like homeTotal. 3 means 3 or more. Pair with participant (Q120). Not outcome line (Q121)."
        }
      },
      "allOf": [
        {
          "$ref": "#/$defs/propOutcomePairing"
        }
      ]
    },

    "provenance": {
      "enum": [
        "official",
        "licensed",
        "observed"
      ]
    },
    "role": {
      "enum": [
        "home",
        "away",
        "neutral"
      ],
      "description": "A fact set by the publisher; never inferred from presentation order."
    },
    "deprecation": {
      "type": "object",
      "required": [
        "name",
        "reason",
        "replacement",
        "sunset"
      ],
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128
        },
        "reason": {
          "type": "string",
          "minLength": 1,
          "maxLength": 512
        },
        "replacement": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128
        },
        "sunset": {
          "type": "string",
          "format": "date",
          "description": "Inclusive last date the name may appear on the live wire; removal only at MAJOR (Q36)."
        }
      }
    },
    "jsonld": {
      "properties": {
        "@context": {},
        "@type": {
          "type": "string"
        }
      }
    },
    "enums": {
      "description": "Every closed vocabulary in OpenBook lives here, once. Schemas $ref these; docs/vocabularies are generated from them."
    },
    "objectType": {
      "enum": [
        "fixture",
        "odds",
        "market",
        "score",
        "grade",
        "league",
        "season",
        "stage",
        "participant",
        "player",
        "lineup",
        "publisher",
        "stall",
        "toss",
        "series"
      ]
    },
    "action": {
      "enum": [
        "snapshot",
        "create",
        "update",
        "delete",
        "change",
        "snapshotComplete",
        "heartbeat"
      ],
      "description": "`change` is used only by `odds`. `snapshotComplete` and `heartbeat` are control actions on the same stream (Q46)."
    },
    "msgType": {
      "enum": [
        "alert",
        "update",
        "cancel"
      ],
      "description": "CAP 1.2 msgType, on market/update."
    },
    "eventStatus": {
      "enum": [
        "scheduled",
        "delayed",
        "live",
        "paused",
        "suspended",
        "postponed",
        "ended",
        "cancelled"
      ],
      "description": "Fixture lifecycle (Q25). `ended` happens once."
    },
    "marketStatus": {
      "enum": [
        "open",
        "suspended",
        "closed",
        "void"
      ],
      "description": "Can you bet it (Q43). Off the board is status, not a sentinel price. Last odds MAY remain when not open."
    },
    "competitionType": {
      "enum": [
        "league",
        "cup",
        "tournament",
        "series",
        "exhibition"
      ]
    },
    "participantType": {
      "enum": [
        "team",
        "individual"
      ]
    },
    "sourceType": {
      "enum": [
        "sportsbook",
        "exchange",
        "model"
      ]
    },
    "feedKind": {
      "enum": [
        "snapshot",
        "stream",
        "docs",
        "mcp",
        "plugin"
      ],
      "description": "What a discovery feed entry is (Q56). snapshot/stream/docs are the OpenBook feed; mcp and plugin point at another standard's manifest, not an OpenBook document."
    },
    "stageType": {
      "enum": [
        "phase",
        "group",
        "round",
        "matchday",
        "leg",
        "seriesGame"
      ]
    },
    "marketShape": {
      "enum": [
        "binary",
        "n-way",
        "over-under",
        "handicap",
        "exact-value",
        "correct-score",
        "yes-no",
        "composite"
      ],
      "description": "How a market type is structured. composite = legs that reference other market outcomes (parlay, same-game parlay)."
    },
    "marketCategory": {
      "enum": [
        "main-line",
        "score-prop",
        "game-prop",
        "player-prop",
        "outright",
        "parlay-special",
        "same-game-parlay"
      ]
    },
    "segmentStatus": {
      "enum": [
        "pending",
        "live",
        "paused",
        "down"
      ],
      "description": "Per-segment status (Q30). `down` = final, happens once, terminal."
    },
    "statusReason": {
      "enum": [
        "weather",
        "crowd",
        "injury",
        "retirement",
        "walkover",
        "forfeit",
        "technical",
        "scheduling",
        "correction",
        "other"
      ]
    },
    "scoreUnit": {
      "enum": [
        "goals",
        "points",
        "runs",
        "hits",
        "errors",
        "sets",
        "games",
        "frames",
        "strokes",
        "laps",
        "position",
        "time",
        "corners",
        "yellowCards",
        "redCards",
        "wickets",
        "overs",
        "rounds",
        "knockdowns",
        "aces",
        "faults",
        "other"
      ],
      "description": "What a score line counts (Q26). The sport/league declares its primaryUnit."
    },
    "gradeResult": {
      "enum": [
        "win",
        "lose",
        "void",
        "half-win",
        "half-lose"
      ]
    }
  }
}
