How do I work with UUID for Unique Identifier Generation in Rust?

Walkthrough

UUID is a crate for generating and parsing Universally Unique Identifiers (UUIDs) in Rust. UUIDs are 128-bit identifiers that are practically unique without requiring a central coordination authority. They're commonly used for database primary keys, session identifiers, and distributed systems.

Key concepts:

  • UUID versions — Different generation strategies (v1, v4, v5, v7)
  • Generation — Create new UUIDs with various methods
  • Parsing — Parse UUIDs from string formats
  • Formatting — Display UUIDs in hyphenated, simple, or URN format
  • Serialization — Serde integration for JSON and other formats

UUID Versions:

Version Description Use Case
v1 Time + MAC address Legacy, time-ordered
v4 Random General purpose, most common
v5 SHA-1 hash + namespace Deterministic, reproducible
v7 Unix timestamp + random Time-ordered, modern

When to use UUID:

  • Database primary keys
  • Session tokens and auth IDs
  • Distributed system identifiers
  • Correlation IDs for tracing
  • Unique filenames

When NOT to use UUID:

  • Short URLs (use short random strings)
  • Sequential IDs when order matters (use snowflake IDs)
  • When human-readable IDs are needed
  • When you need < 128 bits

Code Examples

Basic UUID Generation (v4 - Random)

use uuid::Uuid;
 
fn main() {
    // Generate a random UUID (v4)
    let id = Uuid::new_v4();
    
    println!("UUID: {}", id);
    println!("Hyphenated: {}", id.hyphenated());
    println!("Simple (no dashes): {}", id.simple());
    println!("URN format: {}", id.urn());
}

Nil and Max UUIDs

use uuid::Uuid;
 
fn main() {
    // Nil UUID (all zeros)
    let nil = Uuid::nil();
    println!("Nil: {}", nil);  // 00000000-0000-0000-0000-000000000000
    
    // Max UUID (all ones)
    let max = Uuid::max();
    println!("Max: {}", max);  // ffffffff-ffff-ffff-ffff-ffffffffffff
    
    // Check if nil
    assert!(nil.is_nil());
    assert!(!max.is_nil());
}

Parsing UUIDs

use uuid::Uuid;
 
fn main() {
    // Parse from string
    let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
    println!("Parsed: {}", id);
    
    // Parse from simple format (no hyphens)
    let id_simple = Uuid::parse_str("550e8400e29b41d4a716446655440000").unwrap();
    println!("From simple: {}", id_simple);
    
    // Parse with braces
    let id_braced = Uuid::parse_str("{550e8400-e29b-41d4-a716-446655440000}").unwrap();
    println!("With braces: {}", id_braced);
    
    // Parse from URN
    let id_urn = Uuid::parse_str("urn:uuid:550e8400-e29b-41d4-a716-446655440000").unwrap();
    println!("From URN: {}", id_urn);
}

UUID Version 4 (Random)

use uuid::Uuid;
 
fn main() {
    // v4 is the most common - cryptographically random
    let id1 = Uuid::new_v4();
    let id2 = Uuid::new_v4();
    
    println!("ID 1: {}", id1);
    println!("ID 2: {}", id2);
    
    // Each is unique
    assert_ne!(id1, id2);
    
    // Check version
    println!("Version: {:?}", id1.get_version());  // Some(Random)
    println!("Variant: {:?}", id1.get_variant());  // Some(RFC4122)
}

UUID Version 7 (Time-Ordered)

use uuid::Uuid;
 
fn main() {
    // v7 combines timestamp with randomness - time-ordered
    let id1 = Uuid::now_v7();
    std::thread::sleep(std::time::Duration::from_millis(10));
    let id2 = Uuid::now_v7();
    
    println!("ID 1: {}", id1);
    println!("ID 2: {}", id2);
    
    // v7 UUIDs sort by time
    assert!(id1 < id2);
    
    // Extract timestamp
    if let Some(ts) = id1.get_timestamp() {
        println!("Timestamp: {:?}", ts);
    }
}

UUID Version 5 (Deterministic)

use uuid::{Uuid, uuid5};
 
fn main() {
    // v5 generates deterministic UUIDs from a namespace and name
    // Same namespace + name = same UUID
    
    // DNS namespace
    let dns_namespace = Uuid::NAMESPACE_DNS;
    
    let id1 = Uuid::new_v5(dns_namespace, b"example.com");
    let id2 = Uuid::new_v5(dns_namespace, b"example.com");
    
    println!("UUID for example.com: {}", id1);
    
    // Same input = same output
    assert_eq!(id1, id2);
    
    // Different name = different UUID
    let id3 = Uuid::new_v5(dns_namespace, b"other.com");
    assert_ne!(id1, id3);
}

UUID Version 1 (Time-Based)

use uuid::Uuid;
 
fn main() {
    // v1 uses timestamp and node ID
    // Requires "v1" feature and clock sequence
    
    // Simple approach with random node
    let id = Uuid::now_v1(&[1, 2, 3, 4, 5, 6]);
    println!("v1 UUID: {}", id);
    
    // Get timestamp
    if let Some(ts) = id.get_timestamp() {
        let (seconds, nanos) = ts.to_unix();
        println!("Unix time: {}.{:09}", seconds, nanos);
    }
}

Custom UUID Construction

use uuid::Uuid;
 
fn main() {
    // From bytes (big-endian)
    let bytes: [u8; 16] = [
        0x55, 0x0e, 0x84, 0x00,
        0xe2, 0x9b,
        0x41, 0xd4,
        0xa7, 0x16,
        0x44, 0x66, 0x55, 0x44, 0x00, 0x00
    ];
    let id = Uuid::from_bytes(bytes);
    println!("From bytes: {}", id);
    
    // From 128-bit integer
    let num: u128 = 0x550e8400_e29b_41d4_a716_446655440000;
    let id = Uuid::from_u128(num);
    println!("From u128: {}", id);
    
    // Get bytes back
    let bytes = id.as_bytes();
    println!("Bytes: {:02x?}", bytes);
    
    // Get u128
    let num = id.as_u128();
    println!("As u128: {:032x}", num);
}

Serde Integration

use serde::{Serialize, Deserialize};
use uuid::Uuid;
 
#[derive(Serialize, Deserialize, Debug)]
struct User {
    id: Uuid,
    name: String,
}
 
fn main() {
    let user = User {
        id: Uuid::new_v4(),
        name: "Alice".to_string(),
    };
    
    // Serialize to JSON
    let json = serde_json::to_string(&user).unwrap();
    println!("JSON: {}", json);
    
    // Deserialize from JSON
    let parsed: User = serde_json::from_str(&json).unwrap();
    println!("Parsed: {:?}", parsed);
}

Binary Representation

use uuid::Uuid;
 
fn main() {
    let id = Uuid::new_v4();
    
    // As bytes
    let bytes = id.as_bytes();
    println!("Bytes length: {}", bytes.len());  // 16
    
    // As fields (for RFC 4122 format)
    let (time_low, time_mid, time_hi_and_version,
         clock_seq_hi_and_reserved, clock_seq_low, node) = id.as_fields();
    
    println!("Time low: {:08x}", time_low);
    println!("Time mid: {:04x}", time_mid);
    println!("Time hi: {:04x}", time_hi_and_version);
    
    // As u64 pair
    let (high, low) = id.as_u64_pair();
    println!("High: {:016x}, Low: {:016x}", high, low);
}

Database IDs

use uuid::Uuid;
use serde::{Serialize, Deserialize};
 
#[derive(Serialize, Deserialize, Debug)]
struct Product {
    id: Uuid,
    name: String,
    price: f64,
}
 
impl Product {
    fn new(name: String, price: f64) -> Self {
        Self {
            id: Uuid::new_v4(),
            name,
            price,
        }
    }
}
 
fn main() {
    let product = Product::new("Widget".to_string(), 29.99);
    println!("Product: {:?}", product);
}

Session Tokens

use uuid::Uuid;
use std::collections::HashMap;
 
struct Session {
    user_id: u64,
    created_at: std::time::Instant,
}
 
class SessionManager {
    sessions: HashMap<Uuid, Session>,
    
    fn new() -> Self {
        Self {
            sessions: HashMap::new(),
        }
    }
    
    fn create_session(&mut self, user_id: u64) -> Uuid {
        let token = Uuid::new_v4();
        self.sessions.insert(token, Session {
            user_id,
            created_at: std::time::Instant::now(),
        });
        token
    }
    
    fn get_user(&self, token: &Uuid) -> Option<u64> {
        self.sessions.get(token).map(|s| s.user_id)
    }
    
    fn remove_session(&mut self, token: &Uuid) {
        self.sessions.remove(token);
    }
}
 
fn main() {
    let mut manager = SessionManager::new();
    
    let token = manager.create_session(42);
    println!("Session token: {}", token);
    
    if let Some(user_id) = manager.get_user(&token) {
        println!("User ID: {}", user_id);
    }
    
    manager.remove_session(&token);
}

Correlation IDs

use uuid::Uuid;
use std::thread;
 
fn process_request(request_id: Uuid, data: &str) {
    println!("[{}] Processing: {}", request_id, data);
    
    // Pass ID through call stack
    validate(&request_id, data);
    save(&request_id, data);
}
 
fn validate(request_id: &Uuid, data: &str) {
    println!("[{}] Validating", request_id);
}
 
fn save(request_id: &Uuid, data: &str) {
    println!("[{}] Saving", request_id);
}
 
fn main() {
    let request_id = Uuid::new_v4();
    process_request(request_id, "test data");
}

Short IDs (First 8 Characters)

use uuid::Uuid;
 
fn short_id(id: &Uuid) -> String {
    id.simple().to_string()[..8].to_string()
}
 
fn main() {
    let id = Uuid::new_v4();
    
    println!("Full UUID: {}", id);
    println!("Short ID: {}", short_id(&id));
    
    // Useful for logs, display
    // Note: Not unique! Use only for display
}

Comparison and Ordering

use uuid::Uuid;
 
fn main() {
    let id1 = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
    let id2 = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap();
    let id3 = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
    
    // Comparison
    assert!(id1 < id2);
    assert_eq!(id1, id3);
    
    // Sorting (v7 UUIDs are time-ordered)
    let v7_1 = Uuid::now_v7();
    std::thread::sleep(std::time::Duration::from_millis(1));
    let v7_2 = Uuid::now_v7();
    
    let mut ids = vec![v7_2, v7_1];
    ids.sort();
    assert_eq!(ids, vec![v7_1, v7_2]);
}

Hash Map Keys

use uuid::Uuid;
use std::collections::HashMap;
 
fn main() {
    let mut map: HashMap<Uuid, String> = HashMap::new();
    
    let id1 = Uuid::new_v4();
    let id2 = Uuid::new_v4();
    
    map.insert(id1, "First".to_string());
    map.insert(id2, "Second".to_string());
    
    // Lookup by UUID
    if let Some(value) = map.get(&id1) {
        println!("Found: {}", value);
    }
}

URL-Safe Encoding

use uuid::Uuid;
use base64::{Engine as _, engine::general_purpose};
 
fn uuid_to_base64(id: &Uuid) -> String {
    general_purpose::URL_SAFE_NO_PAD.encode(id.as_bytes())
}
 
fn base64_to_uuid(encoded: &str) -> Option<Uuid> {
    let bytes = general_purpose::URL_SAFE_NO_PAD.decode(encoded).ok()?;
    let arr: [u8; 16] = bytes.try_into().ok()?;
    Some(Uuid::from_bytes(arr))
}
 
fn main() {
    let id = Uuid::new_v4();
    
    let encoded = uuid_to_base64(&id);
    println!("Base64: {}", encoded);  // 22 characters instead of 36
    
    let decoded = base64_to_uuid(&encoded).unwrap();
    assert_eq!(id, decoded);
}

Batch Generation

use uuid::Uuid;
 
fn generate_batch(count: usize) -> Vec<Uuid> {
    (0..count).map(|_| Uuid::new_v4()).collect()
}
 
fn main() {
    let ids = generate_batch(5);
    
    for id in &ids {
        println!("UUID: {}", id);
    }
    
    // All unique
    let unique: std::collections::HashSet<_> = ids.iter().collect();
    assert_eq!(unique.len(), 5);
}

Validation

use uuid::Uuid;
 
fn is_valid_uuid(s: &str) -> bool {
    Uuid::parse_str(s).is_ok()
}
 
fn main() {
    let test_cases = [
        ("550e8400-e29b-41d4-a716-446655440000", true),
        ("550e8400e29b41d4a716446655440000", true),
        ("invalid", false),
        ("550e8400-e29b-41d4-a716", false),  // Too short
    ];
    
    for (input, expected) in test_cases {
        let result = is_valid_uuid(input);
        println!("{}: {} (expected {})", input, result, expected);
    }
}

Error Handling

use uuid::Uuid;
 
fn parse_uuid(input: &str) -> Result<Uuid, uuid::Error> {
    Uuid::parse_str(input)
}
 
fn main() {
    match parse_uuid("550e8400-e29b-41d4-a716-446655440000") {
        Ok(id) => println!("Parsed: {}", id),
        Err(e) => eprintln!("Error: {}", e),
    }
    
    match parse_uuid("not-a-uuid") {
        Ok(id) => println!("Parsed: {}", id),
        Err(e) => eprintln!("Expected error: {}", e),
    }
}

Testing with Fixed UUIDs

use uuid::Uuid;
 
fn main() {
    // Fixed UUID for testing
    let test_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
    
    // Or use constants
    const MY_UUID: &str = "550e8400-e29b-41d4-a716-446655440000";
    let id = Uuid::parse_str(MY_UUID).unwrap();
    
    println!("Test UUID: {}", test_id);
    println!("Constant UUID: {}", id);
}

Copy and Clone

use uuid::Uuid;
 
fn main() {
    let id1 = Uuid::new_v4();
    let id2 = id1;  // Copy (UUID is Copy)
    
    assert_eq!(id1, id2);
    
    // Works in functions
    fn print_uuid(id: Uuid) {  // Takes by value (Copy)
        println!("UUID: {}", id);
    }
    
    print_uuid(id1);
    print_uuid(id1);  // Can use again
}

Summary

UUID Key Imports:

use uuid::Uuid;

Generation Methods:

Method Version Description
Uuid::new_v4() v4 Random UUID
Uuid::new_v5(ns, name) v5 Deterministic from namespace + name
Uuid::now_v7() v7 Time-ordered UUID
Uuid::now_v1(node) v1 Time + node
Uuid::nil() - All zeros
Uuid::max() - All ones

Standard Namespaces (for v5):

Constant Description
NAMESPACE_DNS DNS names
NAMESPACE_URL URLs
NAMESPACE_OID ISO OIDs
NAMESPACE_X500 X.500 DN

Output Formats:

let id = Uuid::new_v4();
 
id.hyphenated();  // "550e8400-e29b-41d4-a716-446655440000"
                id.simple();      // "550e8400e29b41d4a716446655440000"
                id.urn();         // "urn:uuid:550e8400-..."
                id.braced();      // "{550e8400-...}"

Parsing:

// All these work:
Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
Uuid::parse_str("550e8400e29b41d4a716446655440000").unwrap();
Uuid::parse_str("{550e8400-e29b-41d4-a716-446655440000}").unwrap();
Uuid::parse_str("urn:uuid:550e8400-e29b-41d4-a716-446655440000").unwrap();

Key Points:

  • v4 (random) is most common for general use
  • v7 (time-ordered) is best for database keys
  • v5 (deterministic) for reproducible IDs
  • UUID implements Copy, Clone, Hash, Eq, Ord
  • 36 characters in hyphenated format
  • 16 bytes binary
  • Use Uuid::nil() for null placeholder
  • Enable serde feature for serialization
  • UUIDs are practically unique, not guaranteed unique