# shortuuid turns 128 random bits into 22 URL-safe characters

> shortuuid is an MIT licensed Go library that generates concise, unambiguous, URL-safe identifiers by taking a standard library UUID and re-encoding it in base57 with the visually confusing characters removed. The v5 line has no dependencies at all, requires Go 1.27, and the migration notes explain what changed when the encoder interface moved from google/uuid to the standard library uuid package.

**lithammer/shortuuid** — A generator library for concise, unambiguous and URL-safe UUIDs

- Repository: https://github.com/lithammer/shortuuid
- Website: https://pkg.go.dev/github.com/lithammer/shortuuid/v4
- Stars: 1,431 · Forks: 66
- Language: Go
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/lithammer-shortuuid

## The alphabet is the feature

shortuuid is a Go library that generates concise, unambiguous, URL-safe UUIDs, and the README explains both the problem and the mechanism in one paragraph.

The problem is stated first. Often, one needs to use non-sequential IDs in places where users will see them, but the IDs must be as concise and easy to use as possible.

The mechanism follows. shortuuid solves this problem by generating UUIDs using the standard library uuid package and then translating them to base57 using lowercase and uppercase letters and digits, and removing similar-looking characters such as l, 1, I, O and 0.

That last clause is the part that distinguishes this from a generic base encoder. A standard base58 uses the full alphabet minus the zero, the lowercase l, the uppercase I and the uppercase O, and the reason for each exclusion is the same, a human reading the identifier aloud or copying it from a screen would confuse them. The library takes that idea one step further and removes the digit 1 as well, so an identifier read over the phone has fewer ambiguous pairs.

The result is 22 characters. The README's example shows the shape of it.

```go
u := shortuuid.New()
fmt.Println(u) // KwSysDpxcBU9FNhGkn2dCf
```

And the decoding is symmetric.

```go
u, err := shortuuid.DefaultEncoder.Decode("KwSysDpxcBU9FNhGkn2dCf")
// 64d1355f-d052-4bd9-83f4-39b93fb1c01f
```

The project is based on and compatible with the Python library shortuuid, which matters because a team with data on both sides wants the identifiers to be interchangeable, and the Python implementation uses the same base57 alphabet. The licence is MIT, and the module path carries a v5 suffix, github.com/lithammer/shortuuid/v5.

## Version 5 moved the encoder to the standard library

The v5 release is not a feature release so much as an interface change, and the migration section in the README is the most useful part of the documentation for anyone upgrading.

New is unchanged, so code calling it only needs its import path bumped to /v5.

Encoder now names the standard library uuid.UUID rather than github.com/google/uuid.UUID, so a custom encoder stops satisfying the interface until it imports uuid instead. Both types are [16]byte, so a google/uuid value converts with uuid.UUID(v).

That is a real breaking change for anyone with a custom encoder, and it is the kind that produces a compile error rather than a silent behaviour change, which is the good kind. The reason for the change is visible in the go.mod, which lists the module as github.com/lithammer/shortuuid/v5 with a go directive of 1.27 and no require block at all.

v5 requires Go 1.27 and has no dependencies. That is a notable change, because the previous line depended on the google/uuid package, and a library that has dropped its only external dependency has become trivial to audit and trivial to vendor.

The deprecations are also spelled out with the replacement next to each one. NewWithEncoder and NewWithAlphabet still work but are deprecated in favour of NewEncoder. The stated reasons are concrete, NewWithEncoder(enc) is enc.Encode(uuid.New()) at the same cost, and encoding directly also reaches the other UUID versions, while NewWithAlphabet re-sorts the alphabet on every call with no way to avoid it, so a reused encoder is more than twice as fast.

And NewWithNamespace still works with unchanged IDs, but is deprecated, because it guesses the namespace from the name, NameSpaceURL for http and https prefixes matched case-insensitively and NameSpaceDNS for everything else, which is what the Python shortuuid library does. An OID or an X.500 DN hashes under NameSpaceDNS too, which is usually not what someone writing an OID intended.

## v7 identifiers sort by creation time

New returns a v4 UUID, which is random, and there is a v7 constructor that is time ordered, and the README explains the consequence carefully.

```go
shortuuid.NewV4() // random, same as New
shortuuid.NewV7() // time-ordered
```

v7 UUIDs lead with a millisecond timestamp, and base57 keeps that order, so sorting v7 shortuuids as strings sorts them by creation time.

That property is worth pausing on, because it is not something a random identifier gives you and it is not free either. A base57 encoding preserves the ordering of the underlying bytes, so a big-endian timestamp at the front of the UUID survives the encoding and the string sort follows it. The result is an identifier that is both short and lexicographically ordered, which means a database index on that column receives inserts in roughly key order, which is a real performance property for a write heavy table.

The README also shows how to get the creation time back, and it does so in a way that says something about the standard library's state.

```go
// timeV7 returns the time a UUIDv7 was created, as defined in RFC 9562,
// Section 5.7. The bool reports whether u is a v7 UUID at all.
func timeV7(u uuid.UUID) (time.Time, bool) {
	if u[6]>>4 != 7 {
		return time.Time{}, false
	}
	ms := binary.BigEndian.Uint64(u[:8]) >> 16
	return time.UnixMilli(int64(ms)), true
}
```

The comment explains that the standard library uuid package has no accessor for the timestamp, so you read the bits directly, and the first 48 hold the Unix time in milliseconds. The version check, u[6]>>4 != 7, is the standard nibble that identifies a v7 UUID, and the shift of 16 discards the 16 bits of the version and the random portion that sit in the low half of the first 64 bits.

There is also a named constructor for the derived case, which is the name based variant.

```go
shortuuid.NewV5(shortuuid.NameSpaceDNS, "example.com")          // exu3DTbj2ncsn9tLdLWspw
shortuuid.NewV5(shortuuid.NameSpaceURL, "http://example.com")   // T35fvrnVz6SMSdh9y5hs8c
shortuuid.NewV5(shortuuid.NameSpaceOID, "1.2.840.113549")       // HVizdopCKiLaGoTrVJrg9r
```

And the README is precise about a detail that produces a wrong answer if you get it wrong. Pass the bare name, an OID is "1.2.840.113549", not "urn:oid:1.2.840.113549". The namespace is a separate argument, and including the urn prefix in the name hashes a different string, so the identifier is not the one the OID registry would expect.

## Decoding is not validation, and the README says so

There is a warning in the README that is worth reading twice, because it is the kind of thing that turns into a bug report.

Decoding is not a validity check. It rejects characters outside the alphabet and values that need more than 128 bits, but pads input that is too short, so an ID that lost a character decodes to a different UUID rather than failing.

So a truncated identifier, one where a character was dropped in a copy and paste or lost in a log, decodes successfully into a well formed but different UUID. If you use the decoded value as a database key and the key is not found, the natural conclusion is that the record does not exist, and the real cause is that the identifier was corrupted in transit. That is a class of bug that is very hard to find later.

The fix the README suggests is to check the length as well.

```go
func valid(s string) bool {
	_, err := shortuuid.DefaultEncoder.Decode(s)
	return err == nil && len(s) == 22
}
```

The same problem appears with a custom alphabet, and the README handles it in a way that teaches a general lesson about alphabets. A custom alphabet encodes to its own width, and its characters may take more than one byte each, so count runes and ask the encoder for the width.

```go
var (
	enc   = shortuuid.NewEncoder(abc)
	width = utf8.RuneCountInString(enc.Encode(uuid.UUID{}))
)

func valid(s string) bool {
	_, err := enc.Decode(s)
	return err == nil && utf8.RuneCountInString(s) == width
}
```

The width is derived by encoding a zero UUID and counting the runes, which is a neat trick, because it asks the encoder rather than hard coding a number that would be wrong the moment the alphabet changes. And the use of utf8.RuneCountInString rather than len is the same lesson in another form, since a byte count is not a character count when the alphabet contains multi-byte characters.

The README also notes that a custom alphabet needs at least 2 distinct characters, and that sorting and deduplicating the alphabet happens up front, so build the encoder once and reuse it.

## The encoder is an interface, so base58 is a twenty line file

The final piece of the design is that the encoding is pluggable, and the README demonstrates it with the case most people will want, base58 as used by Bitcoin.

The Bring your own encoder section says it directly, and base58 is popular among cryptocurrencies like Bitcoin. The example implements the two methods and passes it to NewWithEncoder.

```go
type base58Encoder struct{}

func (enc base58Encoder) Encode(u uuid.UUID) string {
	return base58.Encode(u[:])
}

func (enc base58Encoder) Decode(s string) (uuid.UUID, error) {
	b := base58.Decode(s)
	if len(b) != 16 {
		return uuid.UUID{}, fmt.Errorf("invalid UUID (got %d bytes)", len(b))
	}
	return uuid.UUID(b), nil
}

func main() {
	enc := base58Encoder{}
	fmt.Println(shortuuid.NewWithEncoder(enc)) // 6R7VqaQHbzC1xwA5UueGe6
}
```

The interface is two methods, Encode taking a uuid.UUID and returning a string, and Decode taking a string and returning a uuid.UUID and an error. That is the whole contract, and it is small enough that writing your own is an afternoon.

The example is careful in two places. The Decode implementation checks that the decoded byte slice is exactly 16 bytes before converting, because a base58 decode of a wrong length string returns something that is not a UUID, and the conversion uuid.UUID(b) would panic on a short slice and silently misread on a long one. And the encoder takes any UUID, so one encoder covers every version, enc.Encode(uuid.NewV7()) and enc.Encode(shortuuid.UUIDv5(shortuuid.NameSpaceDNS, "example.com")) both work, which is a difference from the deprecated NewWithEncoder helper that only ever produced random identifiers.

Decode has to use the same encoder, since the alphabet has to match, and the README shows that too.

## Where shortuuid is the wrong choice

The first wrong case is when a plain UUID is fine. The 36 character hyphenated form is not expensive, and databases and browsers handle it. The reason to reach for shortuuid is a specific place, somewhere a user reads the identifier or copies it, and if you do not have such a place, the library adds a dependency and a decoding step for nothing.

The second wrong case is a custom alphabet with a naive expectation. NewEncoder sorts and deduplicates the alphabet, and the width of the output is a property of the alphabet, not a constant. Anyone who assumes 22 characters and uses a custom alphabet will have a validation function that accepts truncated identifiers, which is the exact bug the README warns about for the default encoder.

The third wrong case is a codebase that needs the v4 line's interface. The v5 encoder interface takes the standard library uuid.UUID, so a custom encoder written against google/uuid stops satisfying it. The conversion is trivial, uuid.UUID(v), since both types are [16]byte, but it is a change to every custom encoder in the codebase, and in a large project that is a pull request with more review surface than the rest of the upgrade.

The fourth wrong case is a team that is not ready for Go 1.27. The v5 line requires Go 1.27 and has no dependencies, and a module with no dependencies is easy to vendor, but the language requirement is a hard floor. The v4.3.0 tag from 2026-08-22 is the line for an older toolchain.

## How it differs from a plain uuid.UUID

The comparison worth making is with using the standard library directly, and the difference in approach is entirely about representation.

A standard uuid.UUID is 16 bytes in memory and 36 characters in text, hyphenated in the canonical form. It carries a version nibble and a variant bits, and it can be parsed from several textual forms. The library does not change any of that, and in the v5 line it does not even change the type you work with, since Encoder takes and returns uuid.UUID.

So shortuuid is a text encoding layer, not a different identifier system. The bytes are the same bytes, the randomness is the same randomness, and the properties of the underlying UUID, whether it is random or time ordered or name derived, are unchanged. What changes is how many characters it takes to write down and whether those characters are safe in a URL and readable by a human.

The second difference is the alphabet. Base57 with the visually ambiguous characters removed is a design decision about human transcription, and it has a cost that a generic encoder does not. The fewer characters in the alphabet, the longer the output, and the output length is fixed at 22 for the default alphabet because 57 to the power of 22 exceeds 2 to the 128 bits of a UUID. A custom alphabet with more characters produces a shorter identifier, and the base58 example in the README produces a 22 character value too, which is a useful reminder that base58 is not shorter than base57 here, it is just a different set of characters.

The third difference is compatibility with the Python library. Because the implementation is based on and compatible with the Python shortuuid, an identifier generated in one language decodes in the other, which is what makes it usable in a mixed stack, and it is the property that a hand rolled base57 encoder would not have.

## Conclusion

shortuuid fits a Go service that needs identifiers short enough to put in a URL, a QR code or a support ticket without the hyphenated UUID taking up space that a human will read aloud. It does not fit a project that needs a custom alphabet but wants the library to keep managing it for you, since a custom alphabet needs its own encoder, and it does not fit a codebase that still depends on google/uuid without converting. Before adopting v5, note that the module path has a v5 suffix, that the encoder interface now takes the standard library uuid.UUID so an existing custom encoder stops compiling until you change its import, that NewWithEncoder, NewWithAlphabet and NewWithNamespace are deprecated, and that the library requires Go 1.27 with no dependencies.

## FAQ

### How do I generate a short UUID in Go?

Call shortuuid.New(), which returns a v4 random UUID encoded in base57, for example KwSysDpxcBU9FNhGkn2dCf. Use shortuuid.NewV7() for a time ordered identifier whose string sort follows creation time, and shortuuid.NewV5(namespace, name) for a name derived one. The module path is github.com/lithammer/shortuuid/v5.

### How do I decode a shortuuid back into a UUID?

Use shortuuid.DefaultEncoder.Decode, which returns a uuid.UUID and an error, for example decoding KwSysDpxcBU9FNhGkn2dCf to 64d1355f-d052-4bd9-83f4-39b93fb1c01f. The README warns that decoding is not a validity check, since short input is padded rather than rejected, so compare the length as well.

### What changed in shortuuid v5?

The Encoder interface now names the standard library uuid.UUID rather than github.com/google/uuid.UUID, so a custom encoder stops satisfying it until it imports uuid instead. NewWithEncoder, NewWithAlphabet and NewWithNamespace are deprecated in favour of NewEncoder, NewEncoder and explicit namespaces, and the v5 line requires Go 1.27 and has no dependencies.

### Can I use a custom alphabet with shortuuid?

Yes, with shortuuid.NewEncoder, which sorts and deduplicates the alphabet up front so the encoder can be built once and reused. The README notes that a custom alphabet needs at least 2 distinct characters, that its characters may take more than one byte each, and that the output width should be derived by encoding a zero UUID and counting runes rather than hard coded.

### How do I read the timestamp from a v7 shortuuid?

The standard library uuid package has no accessor for it, so read the bits directly as the README shows, checking the version nibble with u[6]>>4 != 7 and taking the first 48 bits as Unix milliseconds via binary.BigEndian.Uint64(u[:8]) >> 16. The example cites RFC 9562, Section 5.7.

## Sources

- [License: MIT](https://github.com/lithammer/shortuuid/blob/master/LICENSE)
- [lithammer/shortuuid on GitHub](https://github.com/lithammer/shortuuid)
- [Project website](https://pkg.go.dev/github.com/lithammer/shortuuid/v4)
- [README](https://github.com/lithammer/shortuuid/blob/master/README.md)
- [Releases](https://github.com/lithammer/shortuuid/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/lithammer-shortuuid
