The Official Semver 2.0 Regex
The semver.org specification publishes a canonical validation regex. It is designed to match exactly the strings the spec permits — no leading zeros on numeric identifiers, dot-separated pre-release identifiers, and the restricted character set for build metadata.
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
// Groups:
// 1 → major
// 2 → minor
// 3 → patch
// 4 → pre-release string, dot-separated identifiers (optional)
// 5 → build metadata string, dot-separated (optional)
// Example — 1.2.3-rc.1+build.42
// major = "1", minor = "2", patch = "3"
// pre = "rc.1"
// build = "build.42"Why the Pattern Is That Long
The alternations in groups 1-3 — (0|[1-9]\d*) — exist to forbid leading zeros. A plain \d+ would accept 01.2.3, which the spec rejects. The pre-release group uses the same alternation plus a third branch \d*[a-zA-Z-][0-9a-zA-Z-]* for alphanumeric identifiers (alpha, rc, beta.1). Build metadata has a looser character set because the spec does not use build identifiers for ordering.
The Loose Pragmatic Regex
For most use cases — grabbing versions out of a changelog, parsing filenames likeapp-1.2.3.tar.gz, or scanning package.json diffs — a short loose regex is enough. It accepts leading zeros (silently) and does not anchor to start/end.
const SEMVER_LOOSE = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/g;
"Released 1.2.3 and 2.0.0-beta.1 today.".match(SEMVER_LOOSE);
// → ["1.2.3", "2.0.0-beta.1"]
"app-1.2.3.tar.gz".match(SEMVER_LOOSE);
// → ["1.2.3"]
// Loose variant with v prefix (git tags, GitHub releases):
const TAG = /\bv?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?\b/g;
"v1.2.3".match(TAG); // → ["v1.2.3"]
"version 1.2.3".match(TAG); // → ["1.2.3"]Capture Groups for Parsing
Named groups make downstream code far more readable. Modern JavaScript, Python, PHP, Rust, Go (via regexp.Compile) and Java all support them.
const SEMVER_NAMED = /^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<build>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
const { major, minor, patch, prerelease, build } = SEMVER_NAMED.exec("1.2.3-rc.1+build.42").groups;
// major="1", minor="2", patch="3", prerelease="rc.1", build="build.42"
// For a 2.0 release:
SEMVER_NAMED.exec("2.0.0").groups;
// → { major: "2", minor: "0", patch: "0", prerelease: undefined, build: undefined }Pre-release Identifier Rules
Pre-release identifiers are dot-separated and have strict content rules:
- Numeric identifiers: no leading zeros.
1.2.3-alpha.0OK,1.2.3-alpha.01rejected. - Alphanumeric identifiers: at least one non-digit character.
1.2.3-x7OK,1.2.3-0.7.zOK. - Hyphens are allowed inside identifiers but each identifier must not be empty.
1.2.3-x---rc.yOK,1.2.3-rejected. - Identifiers that look purely numeric sort by numeric value; alphanumeric identifiers sort lexically.
Build Metadata Rules
Build metadata starts with + and uses only [0-9A-Za-z-]. It is ignored for ordering — 1.0.0+a and 1.0.0+b are the same version. The spec permits leading zeros inside build metadata because it is not ordered: 1.0.0+001 is valid. Common uses are commit SHA, build timestamp, and CI build number.
Language-Specific Usage
JavaScript / TypeScript
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
function parseSemver(v) {
const m = SEMVER.exec(v);
if (!m) return null;
return {
major: +m[1], minor: +m[2], patch: +m[3],
prerelease: m[4] ? m[4].split('.') : [],
build: m[5] ? m[5].split('.') : [],
};
}
parseSemver("1.2.3-rc.1+build.42");
// { major: 1, minor: 2, patch: 3, prerelease: ["rc","1"], build: ["build","42"] }
// For comparison semantics, use node-semver:
// import semver from 'semver';
// semver.gt('1.2.3-rc.2', '1.2.3-rc.1'); // truePython
import re
SEMVER = re.compile(
r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)'
r'(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?'
r'(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
)
def is_valid(v: str) -> bool:
return SEMVER.match(v) is not None
is_valid("1.2.3") # True
is_valid("1.2.3-rc.1") # True
is_valid("1.2.3-rc.1+sha.ab") # True
is_valid("01.2.3") # False — leading zero
# For comparison, use python-semver:
# import semver
# semver.compare("1.0.0", "1.0.0-rc.1") # -1 means first is smaller (correct)Go
package main
import "regexp"
var semver = regexp.MustCompile(
"^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)" +
"(?:-((?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)" +
"(?:\\.(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?" +
"(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$")
func IsValidSemver(v string) bool {
return semver.MatchString(v)
}
// Go's regexp uses RE2 which doesn't support lookbehind,
// but the official semver regex only needs quantifiers + alternation
// so RE2 handles it fine.
// For comparison, use github.com/Masterminds/semver.Common Pitfalls
Four-part versions aren't semver
1.2.3.4 is common in .NET and some JVM ecosystems but is not semver 2.0. The official regex rejects it. If you need to accept it, write a separate pattern /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/and handle it in branching code — don't try to merge both forms into one regex.
Two-part versions aren't semver either
1.2 is npm range syntax (treated as >=1.2.0 <1.3.0) and is also valid in Python packaging, but it is not a semver version. The official regex requires all three dot-separated numbers.
Trailing dots and dashes
1.2.3-, 1.2.3+, 1.2.3.. are all rejected because the pre-release and build sub-patterns require at least one identifier after the delimiter and between dots.
Case sensitivity
Semver identifiers are case-sensitive by spec. 1.0.0-RC and 1.0.0-rc are different versions. The official regex preserves case; do not add the i flag when validating.
Semver Regex Cheatsheet
| Goal | Pattern | Notes |
|---|---|---|
| Official strict | semver.org canonical | no leading zeros |
| Loose extraction | /\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g | for scanning text |
| With v prefix | /^v?(\d+)\.(\d+)\.(\d+)/ | git tags |
| Pre-release only | /-([0-9A-Za-z.-]+)/ | after version |
| Build only | /\+([0-9A-Za-z.-]+)$/ | tail of string |
Testing Your Semver Regex
Open the live Regex Tester and paste this test block from the semver spec:
# Valid semver strings — must ALL match
0.0.4
1.2.3
10.20.30
1.1.2-prerelease+meta
1.1.2+meta
1.1.2+meta-valid
1.0.0-alpha
1.0.0-beta
1.0.0-alpha.beta
1.0.0-alpha.beta.1
1.0.0-alpha.1
1.0.0-alpha0.valid
1.0.0-rc.1+build.1
2.0.0-rc.1+build.123
1.2.3-beta
10.2.3-DEV-SNAPSHOT
1.0.0-0A.is.legal
# Invalid — must ALL fail
1
1.2
1.2.3-0123
1.2.3-0123.0123
+invalid
-invalid
1.0.0-
1.0.0+
01.1.1
1.2.3.DEV
1.2-SNAPSHOTWhen to Reach for a Library
Regex validates shape. It cannot answer whether 1.0.0-alpha.10 is greater or less than 1.0.0-alpha.2 — the pre-release-identifier comparison rules require: numeric identifiers compare numerically, alphanumerics compare lexically, and a shorter pre-release is less than a longer one with the same prefix. For sorting, satisfies-range checks (^1.2.3, ~1.2.3), and SAT-solving against dependency constraints, use node-semver, python-semver, Go's Masterminds/semver, or Rust's semver crate.