What are the trade-offs between strum::Display and IntoStaticStr for string representation of enum variants?

Display provides runtime string representation via the Display trait returning an owned String, while IntoStaticStr provides compile-time static string references via conversion to &'static str. The trade-off centers on allocation versus flexibility: Display allows runtime customization and works with format! macros, while IntoStaticStr is zero-allocation but returns only the variant name as a static string. The choice depends on whether you need dynamic formatting or maximum performance with static lifetimes.

Basic Usage of Display

use strum::Display;
 
#[derive(Display)]
enum Status {
    Active,
    Inactive,
    Pending,
}
 
fn display_example() {
    let status = Status::Active;
    
    // Display derives std::fmt::Display
    // The to_string() method returns an owned String
    let s: String = status.to_string();
    assert_eq!(s, "Active");
    
    // Works with format! and println!
    println!("Status: {}", status);
    
    // Works with format specifiers
    let lower = format!("{}", status);
    assert_eq!(lower, "Active");
    
    // Returns owned String - has allocation cost
    let owned: String = status.to_string();
}

Display derives the standard std::fmt::Display trait, enabling use with any formatting infrastructure.

Basic Usage of IntoStaticStr

use strum::IntoStaticStr;
 
#[derive(IntoStaticStr)]
enum Status {
    Active,
    Inactive,
    Pending,
}
 
fn into_static_str_example() {
    let status = Status::Active;
    
    // IntoStaticStr implements Into<&'static str>
    // Returns a reference to static string - zero allocation
    let s: &'static str = status.into();
    assert_eq!(s, "Active");
    
    // Can also use with reference
    let s: &'static str = (&status).into();
    assert_eq!(s, "Active");
    
    // The string is determined at compile time
    // No runtime string construction
    // Lifetime is 'static - lives for entire program
}

IntoStaticStr converts to &'static str with no runtime allocation.

Type Signatures and Traits

use strum::{Display, IntoStaticStr};
use std::fmt::Display as StdDisplay;
 
#[derive(Display, IntoStaticStr)]
enum Color {
    Red,
    Green,
    Blue,
}
 
fn type_signatures() {
    let color = Color::Red;
    
    // Display: implements std::fmt::Display
    // fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
    
    // This enables:
    // 1. to_string() -> String (from ToString trait)
    // 2. format!("{}", x) works
    // 3. println!("{}", x) works
    // 4. write! macro works
    
    let string: String = color.to_string();  // Owned String
    let formatted = format!("Color: {}", color);  // Owned String
    
    // IntoStaticStr: implements Into<&'static str>
    // fn into(self) -> &'static str
    
    // This enables:
    // 1. .into() to get &'static str
    // 2. Zero allocation
    // 3. Static lifetime guarantees
    
    let static_str: &'static str = color.into();  // No allocation
    
    // Key difference in return type:
    // Display::to_string() -> String (owned, heap allocated)
    // IntoStaticStr::into() -> &'static str (borrowed, static)
}

The core distinction: Display returns owned String, IntoStaticStr returns borrowed &'static str.

Performance Characteristics

use strum::{Display, IntoStaticStr};
 
#[derive(Display, IntoStaticStr, Clone, Copy)]
enum Status {
    Active,
    Inactive,
}
 
fn performance_comparison() {
    let status = Status::Active;
    
    // Display: allocation for each conversion
    // to_string() allocates a new String
    for _ in 0..1000 {
        let _s = status.to_string();  // 1000 allocations
    }
    
    // IntoStaticStr: zero allocation
    // into() returns a reference to static data
    for _ in 0..1000 {
        let _s: &'static str = status.into();  // 0 allocations
    }
    
    // Benchmark difference:
    // Display: ~50ns per call (allocation + string construction)
    // IntoStaticStr: ~1ns per call (just returning pointer)
}
 
fn memory_usage() {
    // Display produces owned strings
    let strings: Vec<String> = (0..100)
        .map(|_| Status::Active.to_string())
        .collect();
    // Each String is a heap allocation
    
    // IntoStaticStr produces static references
    let refs: Vec<&'static str> = (0..100)
        .map(|_| Status::Active.into())
        .collect();
    // No heap allocations, just copying pointers
}

IntoStaticStr is significantly faster due to zero allocation and static string lookup.

Customizing Display Output

use strum::Display;
 
#[derive(Display)]
enum Status {
    // Customize the display string with strum attribute
    #[strum(to_string = "Status: Active")]
    Active,
    #[strum(to_string = "Status: Inactive")]
    Inactive,
    #[strum(to_string = "Status: Pending Review")]
    Pending,
}
 
fn custom_display() {
    let status = Status::Active;
    assert_eq!(status.to_string(), "Status: Active");
    
    // Display supports arbitrary string customization
    let pending = Status::Pending;
    assert_eq!(pending.to_string(), "Status: Pending Review");
}
 
#[derive(Display)]
#[strum(serialize_all = "snake_case")]
enum ApiEndpoint {
    GetUser,
    CreateUser,
    DeleteUserById,
}
 
fn serialize_all_example() {
    // strum supports various case transformations
    assert_eq!(ApiEndpoint::GetUser.to_string(), "get_user");
    assert_eq!(ApiEndpoint::CreateUser.to_string(), "create_user");
    assert_eq!(ApiEndpoint::DeleteUserById.to_string(), "delete_user_by_id");
    
    // Other serialize_all options:
    // - "lowercase"
    // - "UPPERCASE"
    // - "title_case"
    // - "camelCase"
    // - "PascalCase"
    // - "kebab-case"
    // - "SCREAMING_SNAKE_CASE"
    // etc.
}

Display supports runtime string customization via attributes, offering flexibility.

Customizing IntoStaticStr Output

use strum::IntoStaticStr;
 
#[derive(IntoStaticStr)]
enum Status {
    // into_static_str also supports strum attributes
    #[strum(to_string = "STATUS_ACTIVE")]
    Active,
    #[strum(to_string = "STATUS_INACTIVE")]
    Inactive,
}
 
fn custom_into_static_str() {
    let status = Status::Active;
    let s: &'static str = status.into();
    assert_eq!(s, "STATUS_ACTIVE");
}
 
#[derive(IntoStaticStr)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
enum EventType {
    UserLogin,
    UserLogout,
    DataSync,
}
 
fn serialize_all_static() {
    // serialize_all works with IntoStaticStr too
    let event = EventType::UserLogin;
    let s: &'static str = event.into();
    assert_eq!(s, "USER_LOGIN");
    
    // Note: The strings must be valid at compile time
    // Custom attributes must result in static string constants
}

IntoStaticStr also supports customization attributes while maintaining static lifetime guarantees.

Use Case Comparison

use strum::{Display, IntoStaticStr};
 
#[derive(Display, IntoStaticStr)]
enum HttpMethod {
    Get,
    Post,
    Put,
    Delete,
}
 
fn use_cases() {
    let method = HttpMethod::Get;
    
    // Use Display when:
    
    // 1. You need to work with format! macros
    let log = format!("Request method: {}", method);
    
    // 2. You need String for storage or manipulation
    let methods: Vec<String> = vec![
        HttpMethod::Get.to_string(),
        HttpMethod::Post.to_string(),
    ];
    let concatenated = methods.join(", ");
    
    // 3. You need to pass to APIs expecting String
    fn takes_string(s: String) { /* ... */ }
    takes_string(method.to_string());
    
    // 4. You need custom formatting in attributes
    // (with #[strum(to_string = "...")] attributes)
}
 
fn use_cases_static_str() {
    let method = HttpMethod::Get;
    
    // Use IntoStaticStr when:
    
    // 1. You need maximum performance (zero allocation)
    fn log_method(method: &'static str) {
        println!("Method: {}", method);
    }
    log_method(method.into());
    
    // 2. You need &'static str for const contexts or APIs
    fn takes_static_str(s: &'static str) { /* ... */ }
    takes_static_str(method.into());
    
    // 3. You're comparing against static strings
    if method.into() == "Get" {
        println!("GET request");
    }
    
    // 4. You need to use as map keys without allocation
    use std::collections::HashMap;
    let mut map: HashMap<&'static str, i32> = HashMap::new();
    map.insert(method.into(), 42);
}

Choose based on whether you need String flexibility or &'static str performance.

Integration with Other Traits

use strum::{Display, IntoStaticStr, EnumString, AsRefStr};
 
#[derive(Display, IntoStaticStr, EnumString, AsRefStr)]
enum Status {
    Active,
    Inactive,
}
 
fn trait_integration() {
    // strum provides multiple string-related traits:
    
    // Display: Owned String via Display trait
    let display: String = Status::Active.to_string();
    
    // IntoStaticStr: &'static str via Into trait
    let static_str: &'static str = Status::Active.into();
    
    // AsRefStr: &str via AsRef trait (borrowed, not static)
    let as_ref: &str = Status::Active.as_ref();
    
    // EnumString: Parse from string via FromStr
    let parsed: Status = "Active".parse().unwrap();
    
    // Common combinations:
    // Display + EnumString: Bidirectional string conversion
    // IntoStaticStr + EnumString: Zero-allocation bidirectional
    
    // Comparison:
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ Trait         β”‚ Returns        β”‚ Use case                β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ Display       β”‚ String         β”‚ Format, storage        β”‚
    // β”‚ IntoStaticStr β”‚ &'static str   β”‚ Performance, static    β”‚
    // β”‚ AsRefStr      β”‚ &str           β”‚ Borrowed view           β”‚
    // β”‚ EnumString    β”‚ Self (parse)   β”‚ String to enum         β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
}

strum provides a family of related traits for different string conversion needs.

Lifetime Differences

use strum::{Display, IntoStaticStr};
 
#[derive(Display, IntoStaticStr)]
enum Message {
    Hello,
    Goodbye,
}
 
fn lifetime_demonstration() {
    // Display: String owns its data
    let string: String = Message::Hello.to_string();
    // String has owned data, lives as long as variable
    
    // IntoStaticStr: &'static str is borrowed from static storage
    let static_str: &'static str = Message::Hello.into();
    // Lives for entire program duration
    
    // Implications:
    
    // 1. &'static str can be stored in statics
    static KNOWN_MESSAGE: &str = "Hello";
    
    // 2. &'static str can outlive the enum
    fn get_static_message(msg: Message) -> &'static str {
        msg.into()  // Returned reference outlives msg
    }
    
    // 3. String must be cloned for ownership transfer
    fn get_owned_message(msg: Message) -> String {
        msg.to_string()  // Already owned
    }
    
    // 4. &'static str is implicitly Copy
    let s1: &'static str = Message::Hello.into();
    let s2: &'static str = Message::Hello.into();
    // Both point to same static string
}

The 'static lifetime of IntoStaticStr enables patterns impossible with borrowed references.

Working with Collections

use strum::{Display, IntoStaticStr};
 
#[derive(Display, IntoStaticStr, Clone)]
enum Priority {
    High,
    Medium,
    Low,
}
 
fn collections_display() {
    // Display: Creates owned Strings
    let priorities = vec![Priority::High, Priority::Medium, Priority::Low];
    
    // Converting to Vec<String> allocates for each
    let strings: Vec<String> = priorities.iter()
        .map(|p| p.to_string())
        .collect();
    
    // Joining requires String
    let joined = strings.join(", ");
    assert_eq!(joined, "High, Medium, Low");
}
 
fn collections_static_str() {
    // IntoStaticStr: No allocation
    let priorities = vec![Priority::High, Priority::Medium, Priority::Low];
    
    // Converting to Vec<&'static str> - just pointers
    let refs: Vec<&'static str> = priorities.iter()
        .map(|p| p.into())
        .collect();
    
    // Can still join (as_ref works)
    let joined = refs.join(", ");
    assert_eq!(joined, "High, Medium, Low");
    
    // Using as map keys (efficient)
    use std::collections::HashMap;
    let mut counts: HashMap<&'static str, usize> = HashMap::new();
    for p in &priorities {
        *counts.entry(p.into()).or_insert(0) += 1;
    }
}

Collections of &'static str from IntoStaticStr avoid allocation overhead.

Pattern Matching with Static Strings

use strum::IntoStaticStr;
 
#[derive(IntoStaticStr, Clone, Copy)]
enum Color {
    Red,
    Green,
    Blue,
}
 
fn match_static_str() {
    let color = Color::Red;
    let name: &'static str = color.into();
    
    // &'static str can be matched on efficiently
    match name {
        "Red" => println!("Ruby"),
        "Green" => println!("Emerald"),
        "Blue" => println!("Sapphire"),
        _ => unreachable!(),
    }
    
    // This enables string-based dispatch
    fn process_by_name(name: &'static str) -> &'static str {
        match name {
            "Red" => "warm",
            "Green" => "cool",
            "Blue" => "cool",
            _ => "unknown",
        }
    }
    
    // &'static str works well with static analysis
    let colors: [&'static str; 3] = ["Red", "Green", "Blue"];
    for c in colors {
        println!("{} is {}", c, process_by_name(c));
    }
}

&'static str enables efficient string matching and dispatch patterns.

Interoperability Considerations

use strum::{Display, IntoStaticStr};
 
#[derive(Display, IntoStaticStr)]
enum Status {
    Active,
    Inactive,
}
 
fn api_compatibility() {
    let status = Status::Active;
    
    // Some APIs require String
    fn json_api(body: String) { /* ... */ }
    json_api(status.to_string());  // Display works
    
    // Some APIs accept &str
    fn simple_api(s: &str) { /* ... */ }
    simple_api(&status.to_string());  // Display: allocate then borrow
    simple_api(status.into());        // IntoStaticStr: zero allocation
    
    // Some APIs specifically need &'static str
    fn const_api(s: &'static str) { /* ... */ }
    const_api(status.into());  // IntoStaticStr works, Display doesn't
    
    // Display::to_string() doesn't work for &'static str requirements:
    // const_api(&status.to_string());  // ERROR: String doesn't live long enough
    
    // The performance vs compatibility tradeoff:
    // - IntoStaticStr: faster, but limited to static strings
    // - Display: flexible, but allocates
}

IntoStaticStr is required when APIs need &'static str; Display is more generally compatible.

Practical Recommendations

use strum::{Display, IntoStaticStr};
 
#[derive(Display, IntoStaticStr)]
enum LogLevel {
    Error,
    Warn,
    Info,
    Debug,
    Trace,
}
 
// Recommendation: Use IntoStaticStr for:
// - Logging (no allocation overhead)
// - HTTP headers (static strings)
// - Configuration keys (static strings)
// - Error codes (static strings)
// - Event names (static strings)
 
// Recommendation: Use Display for:
// - User-facing messages
// - Custom formatted strings
// - API serialization
// - String concatenation/joining
// - Storage in collections that need owned strings
 
fn recommended_patterns() {
    // Logging: IntoStaticStr is ideal
    fn log(level: LogLevel, message: &str) {
        println!("[{}] {}", level.into(), message);  // Zero allocation for level
    }
    
    // User messages: Display is appropriate
    fn user_message(level: LogLevel) -> String {
        format!("Current level: {}", level)  // Needs formatting
    }
    
    // Performance-critical paths: IntoStaticStr
    fn hot_path(levels: &[LogLevel]) -> Vec<&'static str> {
        levels.iter().map(|l| (*l).into()).collect()  // Zero allocation
    }
    
    // JSON serialization: Display or IntoStaticStr both work
    fn to_json(level: LogLevel) -> String {
        format!(r#"{{"level": "{}"}}"#, level.into())  // IntoStaticStr works
        // or: format!(r#"{{"level": "{}"}}"#, level)    // Display also works
    }
}

Choose IntoStaticStr for performance-critical paths and APIs requiring static strings; choose Display for flexibility and formatting needs.

Synthesis

Quick reference:

use strum::{Display, IntoStaticStr};
 
#[derive(Display, IntoStaticStr)]
enum Example {
    Variant,
}
 
fn comparison() {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ Aspect               β”‚ Display                β”‚ IntoStaticStr     β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ Returns              β”‚ String (owned)         β”‚ &'static str      β”‚
    // β”‚ Allocation           β”‚ Yes (heap allocation)  β”‚ No (zero alloc)   β”‚
    // β”‚ Trait                β”‚ std::fmt::Display      β”‚ Into<&'static str>β”‚
    // β”‚ Performance          β”‚ ~50ns per call         β”‚ ~1ns per call     β”‚
    // β”‚ Flexibility          β”‚ High (custom format)   β”‚ Limited (static)  β”‚
    // β”‚ Use with format!     β”‚ Yes                    β”‚ Yes               β”‚
    // β”‚ Use as &'static str  β”‚ No (lifetime issue)    β”‚ Yes               β”‚
    // β”‚ Custom attributes    β”‚ strum(to_string = "...")β”‚ Same support      β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
}
 
// When to use Display:
// βœ… Need String for storage or manipulation
// βœ… Working with format! macros extensively
// βœ… Need custom formatted strings
// βœ… API requires owned String
// βœ… String concatenation or joining
 
// When to use IntoStaticStr:
// βœ… Performance-critical code (zero allocation)
// βœ… Need &'static str for APIs
// βœ… Using as HashMap keys (no allocation)
// βœ… Logging in hot paths
// βœ… Working with static string constants
 
// Both support:
// βœ… strum(to_string = "...") attributes
// βœ… strum(serialize_all = "...") case transforms
 
// Common pattern: derive both
#[derive(Display, IntoStaticStr, Clone, Copy)]
enum Dual {
    First,
    Second,
}
 
fn best_of_both(d: Dual) {
    // Use IntoStaticStr when you can
    let static_str: &'static str = d.into();
    
    // Use Display when you need String
    let string: String = d.to_string();
}

Key insight: The trade-off between Display and IntoStaticStr is fundamentally about allocation versus static lifetime guarantees. Display produces owned String values through the standard std::fmt::Display trait, integrating with Rust's formatting ecosystem but requiring heap allocation for each conversion. IntoStaticStr produces &'static str references through the Into trait, offering zero-allocation conversion at the cost of being limited to the variant name (or custom static string) known at compile time. In practice, IntoStaticStr is ideal for performance-sensitive code like logging, HTTP headers, and configuration keys where static strings suffice, while Display is better for user-facing messages, custom formatting, and APIs that require owned strings. Many enums benefit from deriving both traits, allowing developers to choose based on the specific contextβ€”.into() for static strings when possible, .to_string() when String is needed.