A string is a valid UUID when it is 32 hexadecimal digits in five hyphen-separated groups of 8-4-4-4-12, with bits 48 through 51 naming a version and bits 64 and 65 set to 10. That single rule is the whole answer, and it is the one RFC 9562 defines in its May 2024 revision, the standard that obsoleted RFC 4122 (RFC 9562).
Paste a string in and this page returns a verdict straight away, with the version and variant decoded beside it. The check runs inside your browser, so a production identifier never travels to a stranger's server just to be told it is malformed. Whether you need to generate a UUID or confirm one that already exists, no account, no upload, no signup.
Key Takeaways
- A valid UUID is 32 hex digits in 8-4-4-4-12 groups, joined by four hyphens, 36 characters total.
- Character 14 carries the version. Character 19 must be 8, 9, a, or b.
- RFC 9562 added v6, v7 and v8 in 2024. None of them exist in RFC 4122.
- Format-valid is not authentic. The format carries no issuer signature.
- A v4 from a CSPRNG is unguessable. A v1 can leak a host MAC address.
How Do I Know If This Is a Valid UUID?
A UUID is valid when three things hold: 32 hexadecimal digits in five groups of 8-4-4-4-12 joined by four hyphens, a version digit of 1 through 8 at character 14, and a variant digit of 8, 9, a, or b at character 19 (RFC 9562, Section 4). That is the complete rule. Nothing about it is secret, and nothing needs a round trip.
Run those checks in order and stop at the first failure.
- Group structure. 36 characters: 8, 4, 4, 4, 12, separated by exactly four single hyphens.
- Hexadecimal set. Every one of the 32 non-hyphen characters is
0-9,a-f, orA-F. Upper, lower and mixed case all pass. - Version nibble. Character 14, the first digit of the third group, names a defined version.
- Variant bits. Character 19, the first digit of the fourth group, is
8,9,aorb. Those are the10pattern the RFC requires.
A regex on its own gives you checks 1 and 2. It stays silent on 3 and 4, which is exactly how a hand-typed string with a made-up version nibble gets called valid. That gap is the whole reason a decoder beats a pattern.
What Is the Canonical UUID Format?
A canonical UUID is 32 hexadecimal digits written in five groups of 8-4-4-4-12, separated by four single hyphens, for 36 characters total. The example the specification works from is f81d4fae-7dec-11d0-a765-00a0c91e6bf6, and case is free: Section 4 allows all uppercase, all lowercase, or mixed.
Read it once and you can check any UUID in your head. 36 characters, four hyphens, nothing else. The grammar is worth knowing too, since every library you call applies this same rule.
For pure shape screening, this anchored pattern is what most validators apply:
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$
RFC 9562 expresses the same rule as formal ABNF grammar in Section 4:
UUID = 4hexOctet "-"
2hexOctet "-"
2hexOctet "-"
2hexOctet "-"
6hexOctet
hexOctet = HEXDIG HEXDIG
HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
The template form is xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx, where M marks the version and N marks the variant. Treat lowercase as your normalization target regardless, because it is what most generators emit and what most logs show, even though the grammar does not require it.
What Does a Validator Actually Check?
A validator layers four checks on top of each other: group structure, hexadecimal character set, version nibble, and variant bits. Structure comes first because everything else depends on it. A string split into the wrong group sizes cannot be parsed bit by bit at all. Only after the shape passes do the semantic checks run: which version produced this value, and which specification owns its variant bits.
That last layer is where quick checks fall short. A regular expression confirms the shape but says nothing about which version or variant an identifier claims to be, and a validator that reports both gives you more than a green or red light. The verdict costs one read rather than one round trip, so checking several IDs costs nothing but a paste.
What Does the Version Digit Mean?
The version nibble sits at character 14, the first digit of the third group, and occupies the most significant 4 bits of octet 6, bits 48 through 51 of the UUID. RFC 9562 Section 4.2 defines eight of them.
| Digit | Label | Meaning |
|---|---|---|
| 1 | V1 | Gregorian time-based, 60-bit UTC counter in 100ns intervals; node field can embed a MAC address |
| 2 | V2 | Reserved for DCE Security; rarely seen in the wild |
| 3 | V3 | Name-based, MD5 |
| 4 | V4 | Random or pseudorandom; the most common today |
| 5 | V5 | Name-based, SHA-1 |
| 6 | V6 | Reordered Gregorian time-based, derived from the V1 layout for better database locality |
| 7 | V7 | Unix Epoch time-based, 48-bit millisecond timestamp |
| 8 | V8 | Experimental or vendor-specific, 122 bits implementation-defined |
Eight of the sixteen possible digits name a version. The other eight name none at all, which surprises people the first time a decoder answers unknown.
V6, V7 and V8 Did Not Exist Before 2024
This is the one line worth giving anyone whose code predates RFC 9562. Versions 6, 7 and 8 are new in the 2024 revision. RFC 4122, which ran from 2005 until it was superseded, defined 1 through 5 and nothing else (RFC 4122). A v7 that a 2019 parser rejects as an unknown version is not a broken identifier, it is a newer one, and the text reads identically in the current IETF copy of the specification (IETF Datatracker).
If you are weighing V4 against V7 for a new system, our UUIDv4 vs UUIDv7 breakdown covers the randomness-versus-sortability tradeoff in full.
What Do the Variant Bits Mean?
The variant bits sit at character 19, the first digit of the fourth group, and tell parsers which specification governs the remaining bits. They are the only place in the identifier where the layout changes meaning, and RFC 9562 Section 4.1 defines four patterns.
| Bit pattern | Digits | Owner and status |
|---|---|---|
| 0xxx | 0 to 7 | Reserved for Apollo NCS backward compatibility; includes the Nil UUID |
| 10xx | 8 to b | The RFC 9562 variant; mandatory for modern UUIDs |
| 110x | c to d | Reserved for Microsoft backward compatibility |
| 111x | e to f | Reserved for future definition; includes the Max UUID |
Nearly every UUID moving through APIs and databases shows 8, 9, a or b at position 19. One clarification worth making, because it is widely misreported: RFC 9562 did not invent a new variant value. The 10 pattern is identical to the one RFC 4122 defined. The 2024 revision reorganized the standard, it did not change the variant. Anything else at position 19 signals legacy tooling, a hand-fabricated value, or a reserved sentinel worth investigating.
Spellings that real systems accept
Grammar says one thing; databases often say another. PostgreSQL documents accepting uppercase digits, brace-wrapped values, strings with some or all hyphens removed, and even an extra hyphen after any group of four digits, while always emitting the standard form on output (PostgreSQL). The tolerance picture looks like this.
| Input form | RFC grammar | PostgreSQL |
|---|---|---|
| Lowercase canonical | Yes | Yes |
| UPPERCASE | Yes | Yes |
| MiXeD case | Yes | Yes |
| {braced} | Outside grammar; tolerated by common implementations | Yes |
| Unhyphenated (32 chars) | No; widely accepted by convention | Yes |
| urn:uuid: prefix | Outside grammar; defined in RFC 8141 | No |
| Irregular hyphens | No | Yes |
Braced and URN-style strings are conventions that common tools tolerate, not shapes the RFC grammar defines. The URN form, urn:uuid:..., has its own specification, RFC 8141 (RFC 8141). Terminology travels with the debate too: PostgreSQL notes that some systems call this same data type a GUID.
One more wrinkle: Microsoft represents GUIDs with the first three fields in little-endian byte order, so the same identifier can look byte-swapped in its first three groups compared to RFC text order (Microsoft).
A Format-Valid UUID Is Not an Authentic One
Conflating structure with provenance causes real defects. Structure proves conformance to a published format, not origin. Anyone can hand-write a string that resembles a random V4, and a structurally perfect value can even carry a reserved version nibble that no generator would ever emit. The format carries no issuer signature, so authenticity requires checking issuance records in your own application data. A passport number is real because a government issued it, not because the characters look official, and a UUID earns trust the same way.
The practical defense is version gating: chain a structural check to a requirement that the version equals whatever your system issues. The uuid README demonstrates the pattern as uuidValidateV4(), which combines validate() with a version() === 4 comparison (uuid). Treat unexpected versions as suspicious until a lookup proves otherwise.
Why a v4 UUID is not a secret
The security story splits cleanly by version. A V4 drawn from a cryptographically secure random number generator is not predictable. The browser built-in crypto.randomUUID() pulls from a CSPRNG and returns a 36-character V4, and it has been available across major browsers since March 2022 (MDN). Roughly 122 of its 128 bits come from the generator, which leaves far too much entropy for brute force to matter.
V1 is a different story. Its node field can embed the host MAC address, a detail RFC 9562 documents, which is why many teams reject V1 outright for privacy reasons. Both paths sit on the same primitive underneath, crypto.getRandomValues() (MDN). Neither version should ever be used as a security capability, though. RFC 9562 says plainly that UUIDs are not secrets and must not be relied on for authentication. If you need a token, use a real token.
Validating UUIDs in JavaScript
Reach for the uuid library rather than hand-rolled parsing. Its validate() returns a boolean, version() reports the version (0 for NIL, 15 for MAX), and parse() converts a valid string into a 16-byte Uint8Array (uuid).
import { validate, version } from 'uuid';
const isUuidV4 = (s) => validate(s) && version(s) === 4;
There is no native browser API for checking strings; crypto.randomUUID() only generates them. For fast structure-only screening, an anchored regex works well, provided you remember it stays silent about version and variant semantics. If you want to tune a pattern to your own stack, test a UUID-matching regex in a scratchpad first.
Checking a list of UUIDs in bulk
Batch checking follows the same three steps every time: export the suspect column, paste the values one per line, and review per-entry verdicts with their error hints. This page supports batch input, returns instant status for each line, and accepts entries in any casing.
The workflow earns its keep during migrations. Import pipelines that map vendor exports into your schema routinely carry identifiers of uncertain pedigree, and validating an entire column takes minutes. Flagged rows get corrected at the source rather than failing halfway through a load job, and clean lists mean normalized storage on the first pass. To reshape an export before you validate it, export validated UUIDs to CSV, or if the IDs live inside a payload, validate UUIDs inside JSON.
What PostgreSQL does with a bad UUID
Two failure modes await malformed identifiers, and only one announces itself. Truly broken text, the wrong length or non-hexadecimal characters, raises a Class 22 data exception carrying SQLSTATE 22P02, the code for invalid text representation (PostgreSQL). The insert fails loudly and your API surfaces an error to the caller.
The quiet trouble comes from tolerant spellings instead. A mix of braced, unhyphenated and canonical inputs all insert successfully, yet each one normalizes to standard form on output. The same logical identifier can therefore surface in different textual costumes across joins, exports and reports. The docs describe the column as a 128-bit quantity and note that PostgreSQL generates V4 and V7 natively. Canonicalizing upstream, ideally before import, is what keeps stored values uniform.
The nil and max UUIDs
Two reserved values bend the usual assumptions. The Nil UUID is all zeros, 00000000-0000-0000-0000-000000000000, defined in Section 5.9. The Max UUID is all ones, ffffffff-ffff-ffff-ffff-ffffffffffff, defined in Section 5.10. Both pass a naive structural check, yet neither identifies any resource (RFC 9562).
Libraries acknowledge the pair deliberately: uuid.version() returns 0 for NIL and 15 for MAX (uuid). Inside production data, treat either value as a bug, a placeholder that escaped into the wild, or an injection attempt worth a closer look.
Why validate before values enter your stack
Validation is cheap insurance at three levels. First, interoperability: parsers everywhere assume the Section 4 grammar, and nonconforming strings break them unpredictably. Second, API hygiene: browsers now mint identifiers by default through crypto.randomUUID(), so V4 dominates modern payloads and deserves a gate at ingestion. Third, version policy: enforcing which versions you accept turns a vague format check into a deliberate security control.
A fourth, quieter benefit shows up when you decode timestamps. Versions 1, 6 and 7 embed a creation time you can read straight out of the identifier. When you need to inspect that epoch, convert a v7 Unix-epoch timestamp. Knowing when an ID was minted sometimes tells you more than whether it is well formed.
A Four-Question Verdict Checklist
Before a value reaches your database, run it past four questions. Does it hold 32 hexadecimal digits in 8-4-4-4-12 groups? Does character 14 name a version your system actually issues? Does character 19 fall in the 10 range? And is it neither the Nil nor the Max sentinel? A clean pass on all four means the identifier is well formed and plausible. Anything else deserves a closer look first. Past that gate, normalization keeps stored copies consistent and version rules keep old formats from creeping back in.
