A UUID validation function that only checks string shape — 8-4-4-4-12 hex digits separated by hyphens — will happily accept 00000000-0000-0000-0000-000000000000 or ffffffff-ffff-ffff-ffff-ffffffffffff as valid, even though neither has a legal version nibble under RFC 9562. If you're using UUIDs as database keys or trusting them as opaque tokens, that gap matters. Here's what a correct validator actually checks and where the common regex you'll find online falls short.
What Makes a UUID Structurally Valid
A UUID is 128 bits, rendered as 32 hex digits in five groups: 8-4-4-4-12. Two positions are constrained beyond "any hex digit":
- The version nibble — the first character of the third group — must be
1through8for RFC 9562, indicating which UUID version generated it (1 for timestamp+MAC, 4 for random, 7 for Unix-time-ordered, etc.). - The variant bits — the first character of the fourth group — must be
8,9,a, orbfor the standard RFC variant. Other values indicate a different (legacy or reserved) variant layout.
A regex that ignores both of these will match strings that are the right shape but not actually valid UUIDs — including the all-zeros "nil UUID," which is technically defined by the spec as a special reserved value, not a general-purpose valid identifier for version-checking purposes.
A Version-Aware Regex
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function isValidUUID(value) {
return typeof value === 'string' && UUID_REGEX.test(value);
}
isValidUUID('550e8400-e29b-41d4-a716-446655440000'); // true (v4)
isValidUUID('00000000-0000-0000-0000-000000000000'); // false (invalid version nibble)
isValidUUID('550e8400-e29b-41d4-a716-44665544000'); // false (wrong length)
isValidUUID('550E8400-E29B-41D4-A716-446655440000'); // true (case-insensitive)
This checks version 1 through 8 generically. If you need to validate against a specific version — reject anything that isn't v4, for instance — pin the version digit:
function isValidUUIDv4(value) {
const v4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return typeof value === 'string' && v4Regex.test(value);
}
function isValidUUIDv7(value) {
const v7Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return typeof value === 'string' && v7Regex.test(value);
}
This distinction matters in practice: if your system generates UUIDv7 for new records but you're migrating from a legacy system that used v1, a bare "is this a UUID" check will let both through when you actually need to branch logic on which version you received.
Validating with TypeScript Types
Format validation alone doesn't give you a distinct type — string is still string after the regex passes. If you want the compiler to help enforce "this has been validated" downstream, use a branded type with a type guard:
type UUID = string & { readonly __brand: 'UUID' };
function isValidUUID(value: string): value is UUID {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
}
function lookupById(id: UUID) {
// id is guaranteed to have passed validation at every call site
}
const input: string = req.query.id;
if (isValidUUID(input)) {
lookupById(input); // OK — TypeScript narrows to UUID
}
This prevents unvalidated strings from reaching functions that assume UUID shape, catching the mistake at compile time rather than at a runtime database error.
Using the uuid Package Instead
If you're already pulling in the uuid npm package for generation, it also exports validation and version-extraction helpers, which saves you from maintaining the regex yourself:
import { validate as uuidValidate, version as uuidVersion } from 'uuid';
function isValidUUIDv4(value) {
return uuidValidate(value) && uuidVersion(value) === 4;
}
For a project that only needs the occasional format check, the standalone regex has no dependency cost and is easier to audit at a glance. Reach for the library once you need to generate UUIDs anyway, or need programmatic access to the extracted version/variant rather than just a boolean.
Edge Cases: the Nil and Max UUIDs
RFC 9562 defines two special-purpose UUIDs that a version-aware regex will correctly reject, which sometimes surprises people who expect "all zeros" or "all ones" to count as a generic placeholder:
- Nil UUID —
00000000-0000-0000-0000-000000000000— reserved to mean "no UUID" or an explicitly unset value. Its version nibble is0, which falls outside the1–8range the regex above checks. - Max UUID —
ffffffff-ffff-ffff-ffff-ffffffffffff— reserved as the upper-bound sentinel, useful in range queries. Its version nibble isf, also outside the valid range.
Both are legitimate strings that show up in real systems (a nullable UUID column defaulting to nil rather than NULL, a range query using max as an open upper bound), but they are not valid generated UUIDs and a version-aware validator should not accept them as such. If your application intentionally uses either as a sentinel, check for that value explicitly and separately, rather than loosening the general regex to admit it — loosening the regex to pass the nil UUID also reopens the door to other invalid version nibbles you didn't intend to allow.
const NIL_UUID = '00000000-0000-0000-0000-000000000000';
function isValidOrNilUUID(value) {
return value === NIL_UUID || isValidUUID(value);
}
Validating UUIDs in a Schema Library
If the UUID is one field among several in a request body, validating it inline with a regex works but scatters the check across the codebase. Schema libraries like Zod centralize this alongside the rest of the shape validation:
import { z } from 'zod';
const schema = z.object({
id: z.string().uuid(), // validates RFC 9562 format, including version/variant
name: z.string(),
});
schema.parse({ id: '550e8400-e29b-41d4-a716-446655440000', name: 'test' });
Zod's built-in .uuid() check performs the same version- and variant-aware validation described above, so it isn't a looser shape-only check — but it's worth confirming against your specific library version, since the strictness of .uuid()-style validators has changed across major versions in more than one popular schema library. When in doubt, test the library's actual behavior against the nil UUID and a version-8 (8xxxxxxx-...) custom UUID before trusting it in production validation.
Don't Forget: crypto.randomUUID() Never Needs Validation
If you're generating UUIDs yourself rather than accepting them as input, crypto.randomUUID() — available natively in modern browsers and Node — always produces a spec-compliant UUIDv4. Validation is for UUIDs arriving from outside your control: user-submitted form fields, path parameters, third-party API responses, or rows imported from a legacy system that might not have generated them correctly in the first place.
To quickly test a regex like the one above against a batch of sample strings before wiring it into your codebase, the Regex Tester lets you check matches interactively, and the UUID Generator is a fast way to produce known-good v4 and v7 samples to validate your positive cases against.
Wherever you accept a UUID as external input — route params, query strings, request bodies — validate it with a version-aware check before it reaches a database query or a lookup function, not just a shape check that lets malformed-but-similar-looking strings through.