UUID Regex: Why 8-4-4-4-12 Isn't Enough to Validate (2026)
A UUID regex that only checks the 8-4-4-4-12 shape accepts garbage. Here's how to validate the version and variant nibbles the way RFC 9562 defines them.
UUID Regex: Why 8-4-4-4-12 Isn't Enough
The regex everyone reaches for is [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}. It checks that a string has 32 hex digits split 8-4-4-4-12 with hyphens — the shape of a UUID. The problem is that shape is only half the spec. That pattern happily accepts zzzz... if you forget the class bounds, matches UUIDs buried inside other strings, and validates values that no conformant generator would ever emit. Here's what a UUID regex actually needs to check.
TL;DR
- The 8-4-4-4-12 pattern validates layout, not the version or variant bits.
- The 13th hex digit is the version; RFC 9562 defines versions
1–8. - The 17th hex digit is the variant; standard UUIDs use
8,9,a, orb. - Anchor with
^...$to validate;\bmis-handles UUIDs joined by_. - A strict pattern rejects the nil (all-zero) and max (all-F) UUIDs by design.
What a UUID actually encodes
A UUID is a 128-bit value, written as 32 hexadecimal digits in five hyphen-separated groups — 8, 4, 4, 4, and 12 digits, 36 characters total. But two of those nibbles aren't free hex: they carry structural metadata that a real generator sets deliberately.
The version nibble (13th digit)
The first digit of the third group is the version. Per RFC 9562, it lives in the most significant 4 bits of octet 6, and the currently defined versions are 1 through 8 (time-based, DCE, MD5-name, random, SHA-1-name, reordered-time, Unix-time, and custom). In the canonical string xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx, that's the M. A [0-9a-f] class in that position accepts 0, 9, c, f — none of which correspond to a real UUID version.
The variant nibble (17th digit)
The first digit of the fourth group — the N above — encodes the variant. RFC 9562 UUIDs use the "10xx" variant, meaning the top two bits are fixed at 1 and 0. Only four hex digits have high bits 10: 8 (1000), 9 (1001), a (1010), and b (1011). So the fourth group of any standard UUID always starts with one of those four characters. If you see a UUID whose fourth group starts with c through f, it's a different variant (Microsoft GUID legacy or reserved), and if it starts with 0–7 it's the old NCS variant.
Why the shape-only pattern lets garbage through
Because the naive class permits any hex in those two positions, it validates strings that violate the spec:
9b1deb4d-3b7d-0000-0000-c0ffee123456
That has a perfect 8-4-4-4-12 shape, but its version nibble is 0 and its variant nibble is 0. No generator produces it. A strict pattern rejects it; the shape-only one waves it through.
How to write a UUID regex that validates the version and variant
The fix is two targeted character classes: constrain the version nibble to 1–8 and the variant nibble to 8,9,a,b.
The strict RFC 9562 pattern
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-
[1-8][0-9a-fA-F]{3}-
[89abAB][0-9a-fA-F]{3}-
[0-9a-fA-F]{12}$
Written on one line for use:
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$
The [1-8] at the start of the third group enforces a valid version, and the [89abAB] at the start of the fourth enforces the standard variant. Everything else stays open hex. Paste it, plus a column of real and fake UUIDs, into the regex tester to watch which strings pass before it reaches production code.
Pinning to a single version
If your system only issues v4 (random) UUIDs, don't accept the rest. Swap the version class for a literal:
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-
4[0-9a-fA-F]{3}-
[89abAB][0-9a-fA-F]{3}-
[0-9a-fA-F]{12}$
Now the third group must start with 4. The same trick works for v7 (7), which is increasingly the default for database keys because it sorts by time. Generate a batch of known-good values with the UUID generator and confirm they all match before you deploy the tighter rule.
Shape check vs. semantic check
| Concern | Shape-only regex | Strict regex |
|---|---|---|
| 36 chars, 8-4-4-4-12 | ✅ | ✅ |
| Valid version nibble | ❌ | ✅ |
| Valid variant nibble | ❌ | ✅ |
| Rejects nil / max UUID | ❌ | ✅ |
Use the shape-only pattern when you're merely extracting candidate UUIDs from noisy text and will validate them properly afterward. Use the strict pattern when the match itself is the validation.
Why \b is the wrong anchor for a UUID
The title's \b...\b framing points at a second, subtler bug: word boundaries are the wrong tool for delimiting a UUID.
Anchor validation with ^ and $
When you're validating a whole input — a path parameter, a form field, an API argument — you want the entire string to be a UUID and nothing else. That's ^ and $, not \b. Without anchors, [0-9a-f]{8}-...{12} matches a valid UUID sitting inside not-a-uuid-<uuid>-trailing-junk, and your "validation" passes on invalid input. Anchoring is the single most common fix for a regex that matches too much.
\b breaks on underscores
A word boundary sits between a word character ([A-Za-z0-9_]) and a non-word character. Hyphens are non-word, so \b behaves around the dashes inside a UUID. But underscores are word characters, so a UUID embedded like user_550e8400-e29b-41d4-a716-446655440000 has no boundary between _ and 5. A leading \b fails to match there — exactly the identifier-prefixed case you were trying to catch.
Extracting UUIDs from logs
If you genuinely need to pull UUIDs out of surrounding text, use explicit negative look-around on the hex-and-hyphen set instead of \b:
(?<![0-9a-fA-F-])
[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-
[1-8][0-9a-fA-F]{3}-
[89abAB][0-9a-fA-F]{3}-
[0-9a-fA-F]{12}
(?![0-9a-fA-F-])
The lookbehind and lookahead say "not preceded or followed by another hex digit or hyphen," which cleanly isolates a UUID whether it's touching a space, an underscore, a quote, or a slash.
Case sensitivity, the nil UUID, and the max UUID
Two more things bite people who copy a pattern off the first search result.
Handle uppercase input
RFC 9562 says implementations should output lowercase hex but must accept both cases on input. If any upstream system might hand you 550E8400-E29B-41D4-A716-446655440000, a lowercase-only class silently rejects a valid UUID. You have three options, in rough order of preference:
- Include both cases in every class:
[0-9a-fA-F],[89abAB](what the patterns above already do). - Add the case-insensitive
iflag and keep the classes lowercase. - Normalize first — lowercase the string with a case converter or
.toLowerCase()before matching.
Pick one and be consistent; mixing a lowercase class with no flag is the usual source of "it works on my machine" validation bugs.
The nil and max UUIDs are special
RFC 9562 defines two reserved values. The nil UUID is all zeros:
00000000-0000-0000-0000-000000000000
and the max UUID (new in RFC 9562) is all F's:
ffffffff-ffff-ffff-ffff-ffffffffffff
Both have version and variant nibbles of 0 or f, so the strict pattern rejects them — usually what you want, since they're sentinels, not real identifiers. If your domain legitimately uses the nil UUID as an "unset" marker, match it explicitly first:
^(00000000-0000-0000-0000-000000000000|
[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-
[1-8][0-9a-fA-F]{3}-
[89abAB][0-9a-fA-F]{3}-
[0-9a-fA-F]{12})$
Prefer the platform validator when you have one
Regex is the right tool for extraction and for lightweight input guards. But if you're already in a language with a UUID type, parse instead of pattern-match: Python's uuid.UUID(s) and the browser's structured APIs give you version and variant checks plus a real object, not just a boolean. Reach for regex when there's no native parser handy, when you're grepping logs, or when you want a fast pre-filter before the heavier parse. Whichever you choose, test it against known inputs — the regex tester runs entirely in your browser, so you can paste real production UUIDs without them leaving the tab.
References
- RFC 9562 — Universally Unique IDentifiers (UUIDs) — current UUID spec; cited for the version-nibble placement, the 10xx variant, defined versions 1–8, and the nil/max UUID definitions.
- MDN: Crypto.randomUUID() — browser API that emits lowercase v4 UUIDs, referenced for output casing.
- MDN: Character classes in regular expressions — behaviour of
[...]classes, theiflag, and word boundaries\bused throughout the patterns. - Python uuid — UUID objects according to RFC 9562 — native parser cited as the parse-don't-match alternative, including
.versionand.variant.
Related on iKit
- Generate valid v4 UUIDs to test your pattern against — how the random bits, version, and variant nibbles are actually set, which is exactly what the strict regex checks.
- Match a URL with regex and the edge cases most patterns miss — another validation task where anchoring and optional parts decide whether the pattern is real.
- See how email validation hits the same syntax-only wall — why "matches the shape" and "is actually valid" are different questions for UUIDs and emails alike.
- The regex cheatsheet of 25 patterns you'll use every week — the anchors, character classes, and look-around the UUID patterns above are built from.
- Why your regex fails across JavaScript, PCRE, and Python — lookbehind support and flag differences that affect the extraction pattern in this post.
Related posts
SMS 160-Character Limit: Why Going Over Costs You Money (2026)
The SMS 160-character limit comes from a 140-byte cap. One emoji or smart quote can drop it to 70 and double your bill — here's exactly why.
5 Common Regex Mistakes That Match Too Much (2026)
Five regex mistakes that make a pattern match too much — greedy .*, unescaped dots, missing anchors, loose alternation, nested quantifiers — with the fix for each.
Word-Level Diff vs Line-Level Diff: When to Use Each (2026)
Line-level diff shines for code review; word-level diff catches the one edited word inside a long paragraph. Here's when to reach for each in 2026.