iKit
Technical · 10 min read ·

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 to Validate (2026)

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 18.
  • The 17th hex digit is the variant; standard UUIDs use 8, 9, a, or b.
  • Anchor with ^...$ to validate; \b mis-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 07 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 18 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 i flag 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

Related on iKit

Related posts