What is strum::Display derive and how does it differ from implementing Display manually?

strum::Display is a derive macro that automatically implements the std::fmt::Display trait for enums, generating string representations from variant names. Unlike implementing Display manually with custom formatting logic, strum::Display uses the variant name as the display output by default, with optional customization through attributes. This reduces boilerplate for simple cases while providing escape hatches for custom formatting when needed.

Basic strum::Display Usage

use strum::Display;
 
#[derive(Display)]
enum Status {
    Active,
    Inactive,
    Pending,
}
 
fn main() {
    let status = Status::Active;
    println!("Status: {}", status);  // Prints: Status: Active
    
    let status = Status::Pending;
    println!("{}", status);  // Prints: Pending
}

The derive macro generates a Display implementation that outputs the variant name.

Manual Display Implementation

use std::fmt;
 
enum Status {
    Active,
    Inactive,
    Pending,
}
 
impl fmt::Display for Status {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Status::Active => write!(f, "Active"),
            Status::Inactive => write!(f, "Inactive"),
            Status::Pending => write!(f, "Pending"),
        }
    }
}
 
fn main() {
    let status = Status::Active;
    println!("Status: {}", status);  // Prints: Status: Active
}

Manual implementation requires writing the match statement yourself.

Customizing Display Output with Attributes

use strum::Display;
 
#[derive(Display)]
enum Status {
    #[strum(to_string = "Currently Active")]
    Active,
    #[strum(to_string = "Not Active")]
    Inactive,
    Pending,  // Uses default: "Pending"
}
 
fn main() {
    println!("{}", Status::Active);   // Currently Active
    println!("{}", Status::Inactive); // Not Active
    println!("{}", Status::Pending);  // Pending
}

Use #[strum(to_string = "...")] to customize the display string.

Manual Implementation with Custom Formatting

use std::fmt;
 
enum Status {
    Active,
    Inactive,
    Pending,
}
 
impl fmt::Display for Status {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Status::Active => write!(f, "Currently Active"),
            Status::Inactive => write!(f, "Not Active"),
            Status::Pending => write!(f, "Pending"),
        }
    }
}

Manual implementation requires writing each case explicitly.

Case Transformations

use strum::Display;
 
#[derive(Display)]
#[strum(serialize_all = "snake_case")]
enum HttpMethod {
    Get,
    Post,
    Put,
    Delete,
    Patch,
}
 
fn main() {
    println!("{}", HttpMethod::Get);    // get
    println!("{}", HttpMethod::Post);   // post
    println!("{}", HttpMethod::Delete); // delete
}
 
#[derive(Display)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
enum ErrorCode {
    NotFound,
    InternalError,
    BadRequest,
}
 
fn main() {
    println!("{}", ErrorCode::NotFound);     // NOT_FOUND
    println!("{}", ErrorCode::InternalError); // INTERNAL_ERROR
}
 
#[derive(Display)]
#[strum(serialize_all = "kebab-case")]
enum Color {
    LightBlue,
    DarkRed,
    BrightGreen,
}
 
fn main() {
    println!("{}", Color::LightBlue);   // light-blue
    println!("{}", Color::DarkRed);     // dark-red
}

strum supports various case transformations out of the box.

Manual Implementation with Dynamic Formatting

use std::fmt;
 
enum Temperature {
    Celsius(f64),
    Fahrenheit(f64),
    Kelvin(f64),
}
 
impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Temperature::Celsius(v) => write!(f, "{:.1}°C", v),
            Temperature::Fahrenheit(v) => write!(f, "{:.1}°F", v),
            Temperature::Kelvin(v) => write!(f, "{:.1}K", v),
        }
    }
}
 
fn main() {
    let temp = Temperature::Celsius(22.5);
    println!("{}", temp);  // 22.5°C
    
    let temp = Temperature::Fahrenheit(72.3);
    println!("{}", temp);  // 72.3°F
}

Manual implementation handles variants with data and dynamic formatting.

strum with Enum Variants Containing Data

use strum::Display;
 
#[derive(Display)]
enum Message {
    #[strum(to_string = "Hello, {0}!")]
    Greeting(String),
    #[strum(to_string = "Error code: {0}")]
    ErrorCode(u32),
    #[strum(to_string = "User {name} is {age} years old")]
    UserInfo { name: String, age: u32 },
    Simple,
}
 
fn main() {
    println!("{}", Message::Greeting("Alice".to_string()));  // Hello, Alice!
    println!("{}", Message::ErrorCode(404));                  // Error code: 404
    println!("{}", Message::UserInfo {
        name: "Bob".to_string(),
        age: 30
    });  // User Bob is 30 years old
    println!("{}", Message::Simple);  // Simple
}

strum supports format strings with field placeholders in to_string.

Complex Manual Implementation

use std::fmt;
 
enum HttpStatus {
    Ok,
    NotFound,
    ServerError { code: u16, message: String },
    Redirect(String),
}
 
impl fmt::Display for HttpStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            HttpStatus::Ok => write!(f, "200 OK"),
            HttpStatus::NotFound => write!(f, "404 Not Found"),
            HttpStatus::ServerError { code, message } => {
                write!(f, "{} {}", code, message)
            }
            HttpStatus::Redirect(url) => {
                write!(f, "Redirecting to {}", url)
            }
        }
    }
}
 
fn main() {
    println!("{}", HttpStatus::Ok);  // 200 OK
    println!("{}", HttpStatus::ServerError {
        code: 500,
        message: "Internal Error".to_string()
    });  // 500 Internal Error
}

Manual implementation provides full control over complex formatting logic.

Comparison: Boilerplate Reduction

// With strum::Display - minimal boilerplate
use strum::Display;
 
#[derive(Display)]
enum Direction {
    North,
    South,
    East,
    West,
}
 
// Without strum - repetitive match arms
use std::fmt;
 
enum DirectionManual {
    North,
    South,
    East,
    West,
}
 
impl fmt::Display for DirectionManual {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DirectionManual::North => write!(f, "North"),
            DirectionManual::South => write!(f, "South"),
            DirectionManual::East => write!(f, "East"),
            DirectionManual::West => write!(f, "West"),
        }
    }
}

For simple cases, strum::Display eliminates repetitive match arms.

strum::Display with Additional Attributes

use strum::Display;
 
#[derive(Display)]
enum Priority {
    #[strum(to_string = "🔴 High Priority")]
    High,
    #[strum(to_string = "🟡 Medium Priority")]
    Medium,
    #[strum(to_string = "🟢 Low Priority")]
    Low,
}
 
#[derive(Display)]
#[strum(serialize_all = "title_case")]
enum Role {
    SystemAdmin,
    RegularUser,
    GuestUser,
}
 
fn main() {
    println!("{}", Priority::High);   // 🔴 High Priority
    println!("{}", Role::SystemAdmin); // System Admin
    println!("{}", Role::RegularUser); // Regular User
}

Combine to_string with emojis, prefixes, and case transformations.

Manual Display with Conditional Logic

use std::fmt;
 
enum Level {
    Debug,
    Info,
    Warning,
    Error,
}
 
impl fmt::Display for Level {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Can add conditional logic
        if f.alternate() {
            match self {
                Level::Debug => write!(f, "DBG"),
                Level::Info => write!(f, "INF"),
                Level::Warning => write!(f, "WRN"),
                Level::Error => write!(f, "ERR"),
            }
        } else {
            match self {
                Level::Debug => write!(f, "DEBUG"),
                Level::Info => write!(f, "INFO"),
                Level::Warning => write!(f, "WARNING"),
                Level::Error => write!(f, "ERROR"),
            }
        }
    }
}
 
fn main() {
    println!("{}", Level::Debug);      // DEBUG
    println!("{:#}", Level::Debug);    // DBG
}

Manual implementation can use formatter flags and conditional logic.

Integration with Other strum Features

use strum::{Display, EnumString, IntoStaticStr, VariantNames};
 
#[derive(Display, EnumString, IntoStaticStr, VariantNames)]
enum Color {
    Red,
    Green,
    Blue,
}
 
fn main() {
    // Display
    println!("{}", Color::Red);  // Red
    
    // FromStr (EnumString)
    let color: Color = "Green".parse().unwrap();
    
    // Into &str (IntoStaticStr)
    let s: &str = Color::Blue.into();
    println!("{}", s);  // Blue
    
    // Variant names (VariantNames)
    println!("{:?}", Color::VARIANTS);  // ["Red", "Green", "Blue"]
}

strum::Display integrates with other strum derives for consistent behavior.

strum Limitations

use strum::Display;
 
// strum cannot do this:
// - Access external state in formatting
// - Implement conditional formatting based on formatter flags
// - Use complex logic to determine output
// - Format nested data structures
 
// For these cases, use manual implementation
use std::fmt;
 
enum LogLevel {
    Debug,
    Info,
    Warning,
    Error,
}
 
impl fmt::Display for LogLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let (icon, name) = match self {
            LogLevel::Debug => ("🐛", "DEBUG"),
            LogLevel::Info => ("ℹ️", "INFO"),
            LogLevel::Warning => ("⚠️", "WARN"),
            LogLevel::Error => ("❌", "ERROR"),
        };
        
        // Complex formatting with width and alignment
        write!(f, "{} {:5}", icon, name)
    }
}

Manual implementation enables complex formatting that strum cannot express.

Documentation and Maintenance

use strum::Display;
use std::fmt;
 
// strum::Display - generated code is hidden
// Pros: Less code to maintain
// Cons: Need to understand macro behavior
 
#[derive(Display)]
enum Simple {
    A,
    B,
    C,
}
 
// Manual implementation - explicit and documented
// Pros: Clear behavior, IDE support
// Cons: More boilerplate
 
enum SimpleManual {
    A,
    B,
    C,
}
 
impl fmt::Display for SimpleManual {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Clear what each variant outputs
        match self {
            SimpleManual::A => write!(f, "A"),
            SimpleManual::B => write!(f, "B"),
            SimpleManual::C => write!(f, "C"),
        }
    }
}

Consider maintenance and readability when choosing between approaches.

Performance Considerations

use strum::Display;
use std::fmt;
 
// Both approaches have similar runtime performance
// The derive macro generates similar code to manual implementation
 
#[derive(Display)]
enum Derived {
    Variant1,
    Variant2,
}
 
// strum generates essentially:
impl fmt::Display for Derived {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Derived::Variant1 => write!(f, "Variant1"),
            Derived::Variant2 => write!(f, "Variant2"),
        }
    }
}
 
// No runtime overhead from using the derive macro

The derive macro generates equivalent code to manual implementation.

When to Use Each Approach

use strum::Display;
use std::fmt;
 
// Use strum::Display when:
// - Simple variant-to-string mapping
// - Consistent naming patterns
// - Multiple strum derives needed
// - Reducing boilerplate
 
#[derive(Display)]
#[strum(serialize_all = "snake_case")]
enum HttpHeader {
    ContentType,
    ContentLength,
    CacheControl,
}
 
// Use manual implementation when:
// - Complex formatting logic
// - Conditional output
// - Accessing variant data in formatting
// - Using formatter flags (width, precision, alternate)
 
enum Temperature {
    Celsius(f64),
    Fahrenheit(f64),
}
 
impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Temperature::Celsius(v) => {
                if let Some(precision) = f.precision() {
                    write!(f, "{:.prec$}°C", v, prec = precision)
                } else {
                    write!(f, "{:.1}°C", v)
                }
            }
            Temperature::Fahrenheit(v) => {
                write!(f, "{:.1}°F", v)
            }
        }
    }
}
 
fn main() {
    let temp = Temperature::Celsius(22.567);
    println!("{}", temp);        // 22.6°C
    println!("{:.2}", temp);     // 22.57°C
}

Choose based on complexity and formatting needs.

Comparison Summary

Feature strum::Display Manual Display
Boilerplate Minimal More verbose
Custom strings to_string attribute write! macro
Case transforms Built-in Manual
Format strings Basic {0} placeholders Full format spec
Formatter flags No Yes
Conditional logic No Yes
External state No Yes
Compile-time checks Generated code Explicit code

Synthesis

strum::Display and manual Display implementation serve different needs:

Use strum::Display when:

  • Simple variant name to string conversion
  • Consistent case transformations needed
  • Want to reduce boilerplate
  • Using other strum derives for consistency
  • Static string mappings are sufficient

Use manual implementation when:

  • Complex formatting with variant data
  • Conditional formatting based on formatter flags
  • Need access to external state
  • Dynamic formatting logic required
  • Full control over output

Key insight: strum::Display is a convenience tool for the common case of mapping enum variants to static strings. It handles the 80% case elegantly while manual implementation remains available for complex formatting needs. The derive macro generates code equivalent to what you'd write manually, so there's no runtime cost—only development convenience or explicitness depending on your preference.