threepp
A cross-platform C++20 3D library with Python bindings and the high-level API of three.js.
Competitions, seasons, clubs, matches and league tables in JSON-LD. Record the events; derive the score and the table.
Developer toolsJavaScript · HTMLGitHub @football-schemaOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Football Schema: a JSON-LD vocabulary for competitions, seasons, clubs, matches and league tables
Taken from the repository README
Loading the full README.
Read the full README ↗A canonical, machine-readable model of association football in JSON-LD: competitions, seasons, clubs, grounds, people, matches and league tables, with the Premier League as the reference competition. Modelled on Bitcoin Schema: one schema, many projections.
Record the events; derive everything else. A match records what happened (or, when a source
gives only the score, a reported fullTime). The score, the result, minutes played and every
number in a league table are derived fields (/name in the diagram), defined in the schema and
implemented once in tools/model.js.
| module | layer | contents |
|---|---|---|
core | domain | Competition (with TableZones), Season (with PointsDeductions), Club, Registration, PlayerTotals, Stadium, Person → Player / Manager / Referee; Annotations allowed on any node (sameAs, source, retrieved) |
match | event | Match, Score, Lineup, Appearance, MatchEvent → Goal / Card / Substitution; status, period, goal and card enums |
table | projection | Standing: every column derived from finished matches |
premier-league | competition | Premier League rules: tie-break order, match, squad and season rules |
catalogue | catalogue | Dataset: who publishes the data, on what terms, and a link to every season and any other documents |
forecast | forecast | ForecastModel, Rating, MatchForecast (with GoalLines and ScoreChances), SeasonProjection; ForecastRules. Scored once played (/logLoss, /rps) |
Another league reuses core, match and table and adds its own rules module.
Generated from the schema by tools/gen-class-diagram.js
(also as class-diagram.svg).
classDiagram
direction TB
class Competition {
+string name
+code country
+CompetitionFormat format
+int? tier
+gYear founded
+int? clubsPerSeason
+int? relegationPlaces
+int? pointsForWin
+int? pointsForDraw
+matchesPerClub() derived
+currentSeason() derived
}
class Season {
+string label
+date startDate
+date endDate
+Club[]? promoted
+date? resultsAsOf
+SeasonCoverage? coverage
+champion() derived
}
class PointsDeduction {
+Club club
+int points
+date? date
+string? reason
}
class Club {
+string name
+string shortName
+code tla
+gYear founded
+string? nickname
+string[]? altNames
+hexColour[]? colours
+KitPattern? kitPattern
}
class Registration {
+Player player
+Season season
+int shirtNumber
+bool homegrown
}
class Stadium {
+string name
+string city
+int? capacity
+decimal? latitude
+decimal? longitude
}
class Person {
<<abstract>>
+string name
+string? knownAs
+date dateOfBirth
+code nationality
+age() derived
}
class Player {
+Position position
+Foot preferredFoot
}
class Manager {
+Club? club
}
class Referee {
+bool fifaBadge
}
class TableZone {
+int from
+int to
+ZoneKind kind
}
class Match {
+dateTime kickoff
+bool? timeConfirmed
+int matchweek
+MatchStatus status
+int? attendance
+Club homeTeam
+Club awayTeam
+Stadium venue
+Referee? referee
+homeScore() derived
+awayScore() derived
+result() derived
+halfTimeScore() derived
}
class Lineup {
+Club club
+string formation
+Manager manager
}
class Appearance {
+Player player
+int shirtNumber
+bool starter
+bool captain
+minutesPlayed() derived
}
class MatchEvent {
<<abstract>>
+int minute
+int? addedTime
+Period period
+Club club
+clock() derived
}
class Goal {
+Player scorer
+Player? assist
+GoalKind kind
+creditedTo() derived
}
class Card {
+Person player
+CardColour colour
}
class Substitution {
+Player playerOff
+Player playerOn
+bool concussion
}
class Score {
+int home
+int away
}
class Standing {
+Club club
+Season season
+played() derived
+won() derived
+drawn() derived
+lost() derived
+goalsFor() derived
+goalsAgainst() derived
+goalDifference() derived
+points() derived
+deducted() derived
+position() derived
+form() derived
}
class Dataset {
+string name
+string publisher
+iri? license
+string[]? attribution
+dateTime updated
+string schemaVersion
+Competition[]? competitions
+Season[]? seasons
+iri includes
}
class ForecastModel {
+string name
+string? modelVersion
+string? method
}
class Rating {
+Club club
+ForecastModel model
+decimal value
+date asOf
+int? rank
}
class MatchForecast {
+Match fixture
+ForecastModel model
+dateTime issued
+decimal homeWin
+decimal draw
+decimal awayWin
+decimal? expectedHomeGoals
+decimal? expectedAwayGoals
+decimal? bothTeamsScore
+logLoss() derived
+rps() derived
}
class GoalLine {
+decimal line
+decimal over
}
class ScoreChance {
+int home
+int away
+decimal chance
}
class SeasonProjection {
+Club club
+Season season
+ForecastModel model
+dateTime issued
+decimal expectedPoints
+decimal[] positions
+string? scenario
+championChance() derived
+notFinishing() derived
+zoneChance() derived
}
Competition "1" *-- "0..*" TableZone : zones
Competition "1" *-- "1..*" Season : seasons
Season --> "2..*" Club : clubs
Season "1" *-- "0..*" Match : matches
Season "1" *-- "0..*" PointsDeduction : deductions
Season ..> "0..*" Standing : /table
Club --> "1" Stadium : homeGround
Club "1" *-- "0..*" Registration : squad
Person <|-- Player
Person <|-- Manager
Person <|-- Referee
Match "1" *-- "0..2" Lineup : lineups
Match "1" *-- "0..*" MatchEvent : events
Match "1" *-- "0..1" Score : fullTime
Match "1" *-- "0..1" Score : halfTime
Match "1" *-- "0..1" Score : currentScore
Lineup "1" *-- "11..20" Appearance : appearances
MatchEvent <|-- Goal
MatchEvent <|-- Card
MatchEvent <|-- Substitution
MatchForecast "1" *-- "0..*" GoalLine : goalLines
MatchForecast "1" *-- "0..*" ScoreChance : scores
Data in this vocabulary is JSON-LD with this repo's context.jsonld:
{ "@context": "https://football-schema.github.io/schema/context.jsonld",
"@graph": [ { "@id": "#this", "@type": "Club", "name": "Arsenal Football Club", "tla": "ARS", … } ] }
The package exports loaders, the derivations and a validator:
import { loadSchema, loadDir, loadLinked, score, standing, classify } from 'football-schema/model.js';
import { validate } from 'football-schema/validate.js';
// from disk: every file under a directory
const problems = validate(await loadSchema(), await loadDir('data/')); // [] when valid
// from the web: one address, every linked document (seasons -> matches -> clubs -> stadiums)
const g = await loadLinked('https://melvin.me/public/matchday/data/seasons/2026-27.jsonld');
const season = g.nodes.find((n) => n['@type'] === 'Season');
const matches = g.nodes.filter((n) => n['@type'] === 'Match');
const table = classify(season.clubs.map((c) => standing(c, matches, undefined, { deductions: season.deductions })));
validate checks every node against its class, that references resolve to the right types, and the
Premier League's match and season rules (for example a complete double round-robin).
model.js also runs in a browser as it is, with no install or build: import it from this site, and
loadLinked fetches the data (a path is relative to the page; data on another host needs CORS headers).
matchday's viewer is built this way.
<script type="module">
import { loadLinked, standing, classify } from 'https://football-schema.github.io/schema/tools/model.js';
const g = await loadLinked('https://melvin.me/public/matchday/data/seasons/2026-27.jsonld', { follow: ['clubs', 'matches'] });
</script>
Worked examples: 2012-05-13-mci-qpr.jsonld, Manchester City 3–2
QPR recorded as events only (npm run derive computes its score and table);
2023-24-deductions.jsonld, Everton's and Forest's points deductions
with their source and Wikidata identities; 2012-05-13-forecast.jsonld,
an (illustrative) forecast for that MCI–QPR match in another document, scored by forecastScore(); and
dataset.jsonld, a Dataset that links to them.
So that anyone can build on what you publish:
dataset.jsonld at the
data's root, linking every season. A consumer then needs that one address: loadLinked follows the links.seasons/2026-27.jsonld, matches/2026-27/mw08.jsonld,
clubs/ARS.jsonld#this).application/ld+json with Access-Control-Allow-Origin: *, so pages on other sites can read it.source and retrieved on any node, and the Dataset's attribution for
credit lines your sources' terms require.fixture), so anyone
can score them once it is played. Say which model made them.sameAs (their Wikidata entity) and altNames, so other datasets can be joined to
yours without hand-made name tables.Built on it: matchday, a Premier League scores site, and premelo, Elo ratings and predictions for twenty European leagues.
The schema and the package share one version (schemaVersion in a Dataset says which one data conforms
to). Until 1.0: a new version may add classes, fields and enumeration members, all optional, so data that was
valid stays valid; anything removed or renamed is first marked deprecated for at least one version, and the
change is listed below. IRIs (urn:football:…) never change meaning.
model.js runs in the browser as well as Node (the file system is used only in Node). loadLinked keeps up to parallel documents in flight (a pool, not batches; same order of results) and, given onError, skips a document it cannot read and lists it in errors.PlayerTotals
(a player's league goals, matches, assists and penalties for one club in one season, as of a date).
Player.position, Player.preferredFoot, Registration.shirtNumber and Registration.homegrown are
optional, since few sources have them. loadLinked also follows squad and player.forecast module: ForecastModel, Rating, MatchForecast (GoalLine, ScoreChance),
SeasonProjection and ForecastRules (chances 0 to 1 that add up, issued before kick-off); forecastScore(),
zoneChance() and notFinishing() (a projection's places may add up to less than 1, e.g. under an expulsion scenario). Season.coverage (complete or partial; a partial season is exempt from the round-robin
rules). Dataset.includes, and Dataset.seasons may be empty. loadLinked fetches several documents at once.sameAs, source, retrieved. Club.altNames. PointsDeduction
and Season.deductions, counted by standing() (and Standing.deducted). The catalogue module:
Dataset. loadGraph reads over HTTP; loadLinked follows links from one address.npm run diagram # regenerate class-diagram.svg + docs/class-diagram.mmd
npm run derive # derived values for the examples
npm test # schema closure, diagram freshness, examples, derivations
Zero dependencies; Node 20+. index.html is the schema reference, rendered from schema/*.jsonld.
Terms are named in the urn:football: namespace (so Match is urn:football:Match); the context and
this reference are published at https://football-schema.github.io/schema/.