Guidefor developers5 min read
How to sort Minecraft versions in code after calendar versioning
Semver parsers put 1.21.11 above 26.2 and break. Why Minecraft version strings cannot be compared numerically, and what to sort on instead in code.
Why comparing the name fails
A Minecraft version name is an identifier, not a number. Three separate assumptions break on it.
Lexical sorting fails inside a single line. Compared character by character, 1.21.11 sorts below 1.21.2, because the fourth character is 1 in one and 2 in the other. Every release after .9 in a line is affected.
Semver fails on ordering within a version. Given 26.2-snapshot-8 and 26.2-pre-6, semver compares the prerelease identifiers alphanumerically, gets pre before snapshot, and reports the pre-release as older. The real order is the opposite: snapshots come first, then pre-releases, then release candidates, then the release. Semver also cannot parse 26.2 at all, since it has two components rather than three, and it has nothing to say about 25w46a.
Cross era comparison fails on assumptions that were never written down. Plenty of code assumed the first component is always 1 and treated the rest as the real version. That code now reads 26.2 as either invalid or as major version 26, and neither answer tells it that 26.1 is the direct successor of 1.21.11 with nothing in between.
The shapes a version name can take
All of these are live in the dataset today.
| Shape | Example | Notes |
|---|---|---|
| Pre-classic and alpha | rd-132211, rd-20090515 | No numeric component at all |
| Legacy release | 1.21, 1.21.11 | Two or three components |
| Legacy weekly snapshot | 25w46a | Year, week, letter |
| Legacy pre-release | 1.21.11-pre1 | No separator before the number |
| Legacy release candidate | 1.21.11-rc3 | Same |
| Older pre-release | 1.14 Pre-Release 1 | Contains spaces |
| Calendar release | 26.1, 26.2, 26.1.2 | Year and drop, optional patch |
| Calendar snapshot | 26.3-snapshot-5 | Dash separated throughout |
| Calendar pre-release | 26.2-pre-6 | Dash before the number |
| Calendar release candidate | 26.2-rc-2 | Dash before the number |
| April Fools | 24w14potato, 3D Shareware v1.34 | Arbitrary strings |
Note that the separator convention changed with the era. Legacy used 1.21.11-pre1, calendar uses 26.2-pre-6. A regex written for one does not match the other.
Sort on release order, not on the name
The version manifest is already in chronological order. Read the array position once at import time, store it as an integer, and make that integer the only thing you ever sort on.
curl -s https://launchermeta.mojang.com/mc/game/version_manifest_v2.json \
| jq -r '.versions | reverse | to_entries[] | "\(.key)\t\(.value.id)"' | tail -20
On this site that column is sortIndex, unique across all 903 versions, running from 0 for rd-132211 up to 902 for the newest snapshot. Sorting becomes an integer comparison with no parsing:
SELECT name, era, "releaseDate"
FROM "GameVersion"
WHERE type = 'RELEASE'
ORDER BY "sortIndex" DESC
LIMIT 10;
In application code, keep a map from name to index and compare through it. Anything not in the map is a version you have not imported, which is a data problem to surface rather than a string to guess at.
const order = new Map(versions.map((v) => [v.name, v.sortIndex]));
function compare(a: string, b: string): number {
const ia = order.get(a);
const ib = order.get(b);
if (ia === undefined || ib === undefined) throw new Error(`unknown version: ${ia === undefined ? a : b}`);
return ia - ib;
}
Store an era flag beside it so the interface can group calendar and legacy versions without re-deriving the split from the name. Release date works as a secondary display field, but not as the sort key: several versions share a date, and re-releases muddy it.
Data version as a numeric fallback
The data version is a monotonically increasing integer that Mojang bumps for world format changes. It is tempting as a sort key because it is already a number, and it is the right tool when you are reading a world save rather than a name. As a general ordering it has four defects, all present in the current data:
- It does not always exist. 126 of 903 versions carry no data version, including everything before 1.4.5.
- Early values are negative.
1.4.5reports -45 and1.4.6reports -43. - Values repeat.
15w33aand15w33bboth report 111,16w43aand16w44aboth report 817,18w43aand18w43bboth report 1902. - It moves backwards across parallel branches.
20w51areports 2687 and the later1.16.5-rc1reports 2585, because the 1.16.5 branch forked earlier. The same happens at the era boundary, where26w14areports 5000 and the later26.2-snapshot-1reports 4883.
Use it to break ties within one branch, or to identify a world, and read the data version guide before relying on it further.
Snapshots, pre-releases and release candidates
Do not infer the type from the name either. Store it. The manifest distinguishes the types, and the calendar era added its own spellings.
Protocol numbers are a related trap. Modern snapshots, pre-releases and release candidates set bit 30 of the protocol version, so 26.3-snapshot-5 reports 1073742151 while release 26.2 reports 776. No release has the high bit set, and the maximum release protocol today is 776. Sorting a mixed list on protocol therefore puts every recent snapshot above every release. The convention also did not exist for older snapshots, so it is not a reliable type test on its own. The protocol version guide covers what the number is actually for.
April Fools builds and other outliers
Nine April Fools versions exist, from 15w14a in 2015 to 26w14a in 2026. They sit in the manifest in chronological position, which is correct, but they are not on the main development line: 24w14potato reports data version 3824 while the earlier 24w13a reports 3826. Flag them at import and exclude them from any "latest snapshot" query. 1.RV-Pre1, 3D Shareware v1.34 and 23w13a_or_b also carry no data version at all, so any code path that assumes one will fail on them.
Test cases worth keeping
Pin these as assertions. Each one catches a different class of bug.
| Assertion | Catches |
|---|---|
1.21.11 is newer than 1.21.2 | Lexical sorting |
26.2 is newer than 1.21.11 | Era assumptions and ^1\. regexes |
26.2-snapshot-8 is older than 26.2-pre-6 | Semver prerelease ordering |
26.2-pre-6 is older than 26.2-rc-2 | Stage ordering in the calendar era |
26.2-rc-2 is older than 26.2 | Release ranks above its candidates |
25w46a is older than 1.21.11 | Snapshot names against release names |
1.21.11 is the immediate predecessor of 26.1 | The era boundary has no gap |
24w14potato is excluded from latest snapshot | April Fools handling |
What could change
Mojang has changed the scheme twice in one year, so treat any parsing rule as temporary. A third calendar component beyond 26.1.2, a new snapshot spelling, or another naming experiment would all land in the manifest before they land anywhere else. The defence is the same in each case: sort on the imported order, never on the string.
To check, re-fetch the manifest with the command above and compare the tail against the version list on this site. If the newest entry there does not match latest.snapshot in the manifest, our sync is behind, which is visible on the status page.