Validating a UUID means answering two separate questions. The first is whether the string is well-formed, which a pattern can decide. The second is whether it is the kind of identifier the system expects, which requires reading the version and variant fields inside it.

Most validation code answers only the first question and treats any thirty-six character hexadecimal string as acceptable. That is usually fine for routing, and insufficient whenever the version carries meaning.

The canonical format

A UUID in text form is thirty-two hexadecimal digits arranged as 8-4-4-4-12 with four hyphens, giving thirty-six characters in total. Hexadecimal digits are case-insensitive on input; lowercase is the conventional output and the form most systems compare against.

Other representations exist. Microsoft tooling often wraps a GUID in braces, some systems strip the hyphens to a thirty-two character string, and URN form prefixes the value with urn:uuid:. Decide at the boundary which forms you accept, normalise on the way in, and store one canonical shape.

Validating the structure

A regular expression is adequate for a format check. Requiring the version digit and the variant character makes it stricter: the third group begins with the version, and the fourth begins with 8, 9, a, or b for the standard variant.

Prefer a library parser over a hand-rolled UUID v4 checker, because it handles the alternative representations and returns a typed value rather than a string. Anchor any pattern you do write, since an unanchored expression happily matches a valid UUID embedded in a longer hostile string.

Reading the version and variant

The version digit sits at position fifteen of the canonical string, the first character of the third group. A 4 indicates random generation, 7 indicates a time-ordered identifier, 5 indicates a name-based value derived with SHA-1, and 1 indicates the legacy time-and-node format.

The variant occupies the top bits of the ninth byte, visible as the first character of the fourth group. Values from 8 to b mean the RFC variant. Anything else signals a legacy Microsoft layout or a string that merely resembles a UUID, such as a random hexadecimal identifier that was never generated as one.

Why the nil and max values matter

The nil UUID is all zeros and the max UUID is all f digits. Both are structurally valid and neither identifies anything. They appear constantly in practice: as an uninitialised default, as a placeholder written by a failed migration, or as a sentinel in test fixtures.

Validation that only checks the pattern will accept both. If a nil UUID reaching your database would be a bug, reject it explicitly rather than assuming a format check covers it, and log the rejection so you can find the code path that produced it.

UUID v4 collision probability in realistic terms

A version 4 UUID has 122 random bits, giving roughly 5.3 times ten to the thirty-sixth possible values. Applying the birthday bound, you would need to generate about 2.7 times ten to the eighteenth identifiers before reaching a one in two chance of a single collision.

At a billion identifiers per second, that is over eighty years of continuous generation. For any application workload, the probability of collision is far below the probability of undetected hardware error. The practical risk is never the mathematics; it is a weak random source, a duplicated seed in a container image, or code that reuses an identifier by mistake.

Storing identifiers efficiently

A UUID is sixteen bytes. Stored as a canonical string it occupies thirty-six, and as a fixed-width character column it may consume more once the character set is taken into account. Across a large table with foreign keys, the difference is substantial in both disk and index memory.

Use a native type where the database provides one, such as PostgreSQL's uuid, or a sixteen-byte binary column otherwise. Convert to text only at the presentation boundary. Comparing identifiers as binary is also faster and sidesteps every case-sensitivity question.

Decoding what an identifier reveals

Running an identifier through a UUID decoder shows its version, variant, and, for time-based versions, the embedded timestamp. This is a quick way to confirm that a producer is generating what it claims and to understand what a stored value discloses.

Use that information when deciding what to expose publicly. A version 4 value carries nothing beyond itself, a version 7 value carries creation time, and a version 1 value can carry the generating host's network address. None of them are secrets, so a UUID should identify a resource, never authorise access to it.

Size, characters, and layout

The numbers are fixed by the specification and worth stating once. The UUID v4 length in canonical text form is thirty-six characters, of which thirty-two are hexadecimal digits and four are hyphens. The UUID v7 character length is identical, because the format is shared across versions; only the meaning of the bits differs.

PropertyValue
UUID v4 bits, total128
UUID v4 bits, random122
UUID v4 bytes, binary storage16
UUID v4 character length, canonical36 with hyphens, 32 without
UUID v4 characters allowed0-9, a-f, and the hyphen
Version digit position15th character, start of the third group
Variant character20th character: 8, 9, a, or b

Comparing versions and decoding one

The UUID v4 vs v5 choice is randomness against determinism: v4 for identifiers that only need to be unique, v5 when the same input must always produce the same identifier, such as deriving a stable key from a URL. Neither embeds a timestamp, so neither sorts meaningfully.

A UUID v4 decoder and a UUID v7 decoder differ in what there is to report. For v4 the only recoverable facts are the version and variant, since the rest is random by design. A UUID v7 decoder additionally extracts the embedded creation time, which is what makes a UUID v7 checker useful for confirming that a producer is generating identifiers with the clock you expect.

On collisions, the arithmetic in the previous section applies to both. A UUID v7 collision probability calculation covers only the 74 random bits rather than 122, and within a single millisecond that still leaves a figure far below any practical concern, provided the implementation draws those bits from a cryptographic source.

Validating at the right boundary

Check the format once, where untrusted input enters the system, and treat the value as parsed everywhere downstream. Re-validating the same identifier at every layer adds cost without adding safety, while validating nowhere lets a malformed string travel until it fails inside a query.

Return a clear error when a check fails. A response that distinguishes a malformed identifier from one that is well-formed but not found tells an integrator which problem to fix, and it costs nothing in security terms because the format rules are public. What should not differ is the response for a resource that exists but is not accessible, since that distinction leaks the existence of records.

Primary source: RFC 9562 defines UUID versions 1 through 8, including the layout and ordering guarantees discussed above.