How does strum::Display derive macro differ from implementing std::fmt::Display manually for enums?

strum::Display is a derive macro that automatically implements std::fmt::Display for enums by using each variant's name as its display representation. This differs from manual Display implementation in two key ways: it eliminates boilerplate by generating the implementation automatically, and it provides customization options through attributes like #[strum(to_string = "...")] to override default behavior. Manual implementation gives you full control over output formatting and can encode arbitrary logic, while strum::Display provides a declarative approach that keeps the output format visible in the type definition itself. The choice between them depends on whether you need simple variant name mapping (use strum) or complex formatting logic (manual implementation).

Basic strum::Display Usage

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

The derive macro generates a Display implementation using variant names.

Manual Display Implementation

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

Manual implementation requires explicit match arms for each variant.

Customizing Output with strum Attributes

use strum::Display;
 
#[derive(Display)]
enum Status {
    #[strum(to_string = "Waiting for review")]
    Pending,
    #[strum(to_string = "Work in progress")]
    InProgress,
    #[strum(to_string = "Successfully completed")]
    Completed,
    #[strum(to_string = "Operation failed")]
    Failed,
}
 
fn main() {
    println!("{}", Status::Pending);     // "Waiting for review"
    println!("{}", Status::InProgress);  // "Work in progress"
}

#[strum(to_string = "...")] customizes the display string for each variant.

Manual Implementation with Custom Formatting

use std::fmt;
 
enum Status {
    Pending,
    InProgress,
    Completed,
    Failed,
}
 
impl fmt::Display for Status {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Status::Pending => "Waiting for review",
            Status::InProgress => "Work in progress",
            Status::Completed => "Successfully completed",
            Status::Failed => "Operation failed",
        };
        write!(f, "{}", s)
    }
}

Manual implementation achieves the same result with explicit match.

strum with Serialized Names

use strum::Display;
use serde::Serialize;
 
#[derive(Display, Serialize)]
#[strum(serialize_all = "snake_case")]
enum ErrorCode {
    InvalidInput,
    NetworkTimeout,
    AuthenticationFailed,
    InternalError,
}
 
fn main() {
    println!("{}", ErrorCode::InvalidInput);        // "invalid_input"
    println!("{}", ErrorCode::AuthenticationFailed); // "authentication_failed"
}

#[strum(serialize_all = "...")] applies case transformations to all variants.

Case Transformation Options

use strum::Display;
 
#[derive(Display)]
#[strum(serialize_all = "snake_case")]
enum SnakeCase {
    MyVariant,  // "my_variant"
}
 
#[derive(Display)]
#[strum(serialize_all = "camelCase")]
enum CamelCase {
    MyVariant,  // "myVariant"
}
 
#[derive(Display)]
#[strum(serialize_all = "PascalCase")]
enum PascalCase {
    MyVariant,  // "MyVariant"
}
 
#[derive(Display)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
enum ScreamingSnake {
    MyVariant,  // "MY_VARIANT"
}
 
#[derive(Display)]
#[strum(serialize_all = "kebab-case")]
enum KebabCase {
    MyVariant,  // "my-variant"
}
 
fn main() {
    println!("{}", SnakeCase::MyVariant);     // my_variant
    println!("{}", CamelCase::MyVariant);     // myVariant
    println!("{}", PascalCase::MyVariant);    // MyVariant
    println!("{}", ScreamingSnake::MyVariant); // MY_VARIANT
    println!("{}", KebabCase::MyVariant);     // my-variant
}

Multiple case transformations are available via serialize_all.

Manual Implementation with Formatting Logic

use std::fmt;
 
struct User {
    name: String,
    id: u64,
}
 
enum Permission {
    Read,
    Write,
    Admin,
}
 
impl fmt::Display for Permission {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Can access formatter options
        match self {
            Permission::Read => {
                if f.alternate() {
                    write!(f, "READ_ACCESS")
                } else {
                    write!(f, "read")
                }
            }
            Permission::Write => {
                if f.alternate() {
                    write!(f, "WRITE_ACCESS")
                } else {
                    write!(f, "write")
                }
            }
            Permission::Admin => {
                if f.alternate() {
                    write!(f, "ADMIN_ACCESS")
                } else {
                    write!(f, "admin")
                }
            }
        }
    }
}
 
fn main() {
    let perm = Permission::Read;
    println!("{}", perm);      // "read"
    println!("{:#}", perm);    // "READ_ACCESS"
}

Manual implementation can use Formatter options like alternate().

strum Limitations: No Formatter Access

use strum::Display;
 
#[derive(Display)]
enum Permission {
    #[strum(to_string = "read")]
    Read,
    #[strum(to_string = "write")]
    Write,
    #[strum(to_string = "admin")]
    Admin,
}
 
// strum::Display cannot access Formatter options
// Cannot implement: println!("{:#}", Permission::Read);
// All variants always produce the same string

strum::Display generates static strings without formatter access.

Enum with Data: strum Approach

use strum::Display;
 
#[derive(Display)]
enum Message {
    #[strum(to_string = "Hello, World!")]
    Hello,
    #[strum(to_string = "Goodbye")]
    Goodbye,
    #[strum(disabled)]  // Don't derive Display for this variant
    Custom(String),
}
 
// Error: Custom variant has no Display implementation
// Must implement manually or use different approach

strum::Display has limited support for variants with data.

Enum with Data: Manual Implementation

use std::fmt;
 
enum Message {
    Hello,
    Goodbye,
    Custom(String),
}
 
impl fmt::Display for Message {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Message::Hello => write!(f, "Hello, World!"),
            Message::Goodbye => write!(f, "Goodbye"),
            Message::Custom(s) => write!(f, "{}", s),
        }
    }
}
 
fn main() {
    println!("{}", Message::Hello);                   // "Hello, World!"
    println!("{}", Message::Goodbye);                 // "Goodbye"
    println!("{}", Message::Custom("Custom msg".into())); // "Custom msg"
}

Manual implementation handles variants with data naturally.

Combining strum and Manual Display

use strum::Display;
use std::fmt;
 
#[derive(Display)]
enum Status {
    Active,
    Inactive,
    Pending,
}
 
// Additional trait implementations alongside strum::Display
impl fmt::Debug for Status {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Status::Active => write!(f, "Status::Active"),
            Status::Inactive => write!(f, "Status::Inactive"),
            Status::Pending => write!(f, "Status::Pending"),
        }
    }
}
 
fn main() {
    let status = Status::Active;
    println!("Display: {}", status);  // "Active"
    println!("Debug: {:?}", status);  // "Status::Active"
}

strum::Display coexists with manual implementations of other traits.

Multiple Attributes on One Variant

use strum::Display;
 
#[derive(Display)]
enum HttpError {
    #[strum(to_string = "Bad Request")]
    BadRequest,
    #[strum(to_string = "Unauthorized")]
    Unauthorized,
    #[strum(to_string = "Not Found")]
    NotFound,
    #[strum(to_string = "Internal Server Error")]
    InternalError,
}
 
fn main() {
    let err = HttpError::NotFound;
    println!("Error: {}", err);  // "Error: Not Found"
}

Each variant can have its own display string.

Documentation Visibility

use strum::Display;
 
// strum keeps display strings visible in the enum definition
#[derive(Display)]
enum LogLevel {
    Debug,    // Display: "Debug"
    Info,     // Display: "Info"
    Warning,  // Display: "Warning"
    Error,    // Display: "Error"
}
 
// With manual implementation, you must look at impl block
// to see what strings are produced

strum::Display keeps the mapping visible alongside variant definitions.

strum and FromStr Integration

use strum::{Display, EnumString};
 
#[derive(Display, EnumString)]
#[strum(serialize_all = "snake_case")]
enum Color {
    Red,
    Green,
    Blue,
}
 
fn main() {
    // Display produces snake_case
    let color = Color::Red;
    println!("{}", color);  // "red"
    
    // EnumString parses snake_case
    let parsed: Color = "red".parse().unwrap();
    assert_eq!(parsed, Color::Red);
}

Combine Display with EnumString for round-trip parsing.

Round-Trip Consistency

use strum::{Display, EnumString};
 
#[derive(Display, EnumString, Debug, PartialEq)]
#[strum(serialize_all = "snake_case")]
enum State {
    NotStarted,
    InProgress,
    Completed,
}
 
fn main() {
    // Display -> String
    let state = State::InProgress;
    let display = format!("{}", state);  // "in_progress"
    
    // String -> FromStr
    let parsed: State = display.parse().unwrap();
    
    assert_eq!(state, parsed);
}

Consistent serialize_all ensures Display and FromStr align.

Manual Implementation for Complex Formatting

use std::fmt;
 
enum Money {
    Dollars(u32),
    Euros(u32),
    Yen(u32),
}
 
impl fmt::Display for Money {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Money::Dollars(amount) => {
                if *amount == 1 {
                    write!(f, "${}", amount)
                } else {
                    write!(f, "${} dollars", amount)
                }
            }
            Money::Euros(amount) => write!(f, "€{}", amount),
            Money::Yen(amount) => write!(f,{}", amount),
        }
    }
}
 
fn main() {
    println!("{}", Money::Dollars(1));   // "$1"
    println!("{}", Money::Dollars(5));   // "$5 dollars"
    println!("{}", Money::Euros(10));   // "€10"
}

Manual implementation handles conditional formatting logic.

strum Macro Expansion

// What strum::Display generates:
#[derive(Display)]
enum Status {
    Active,
    Inactive,
}
 
// Approximately generates:
impl std::fmt::Display for Status {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Status::Active => write!(f, "Active"),
            Status::Inactive => write!(f, "Inactive"),
        }
    }
}

The macro generates standard Display implementations.

Performance Characteristics

use strum::Display;
 
#[derive(Display)]
enum Fast {
    A,
    B,
    C,
}
 
// strum::Display generates:
// - Static strings (no allocation)
// - Simple match expression
// - Same performance as manual implementation
 
// Manual implementation with formatting:
impl std::fmt::Display for Fast {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // Can potentially allocate or compute
        let s = format!("{:?}", self);  // Allocation
        write!(f, "{}", s)
    }
}

strum::Display uses static strings; manual can introduce allocations.

strum Message Variant

use strum::Display;
 
#[derive(Display)]
enum Notification {
    #[strum(to_string = "You have a new message")]
    NewMessage,
    #[strum(to_string = "You have {0} unread messages")]
    UnreadCount(u32),  // This won't work - strum doesn't support this
}
 
// strum::Display doesn't support string interpolation
// For variants with data, use manual implementation

strum::Display cannot use variant data in output strings.

Manual Implementation with Variant Data

use std::fmt;
 
enum Notification {
    NewMessage,
    UnreadCount(u32),
    CustomMessage(String),
}
 
impl fmt::Display for Notification {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Notification::NewMessage => write!(f, "You have a new message"),
            Notification::UnreadCount(n) => write!(f, "You have {} unread messages", n),
            Notification::CustomMessage(msg) => write!(f, "{}", msg),
        }
    }
}
 
fn main() {
    println!("{}", Notification::UnreadCount(5));  // "You have 5 unread messages"
}

Manual implementation accesses variant data for formatted output.

When to Use strum::Display

use strum::Display;
 
// Good use case: simple variant-to-string mapping
#[derive(Display)]
enum HttpStatus {
    #[strum(to_string = "200 OK")]
    Ok,
    #[strum(to_string = "201 Created")]
    Created,
    #[strum(to_string = "400 Bad Request")]
    BadRequest,
    #[strum(to_string = "404 Not Found")]
    NotFound,
    #[strum(to_string = "500 Internal Server Error")]
    InternalError,
}

Use strum::Display for simple, static string mappings.

When to Use Manual Display

use std::fmt;
 
// Use manual when:
// 1. Variant data affects output
// 2. Formatter options change output
// 3. Complex logic required
// 4. Dynamic string construction
 
enum Error {
    Io(std::io::Error),
    Parse(String),
    Network { code: u16, message: String },
}
 
impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::Io(e) => write!(f, "IO error: {}", e),
            Error::Parse(s) => write!(f, "Parse error: {}", s),
            Error::Network { code, message } => write!(f, "Network error ({}): {}", code, message),
        }
    }
}

Use manual Display for complex formatting or data-dependent output.

Comparison Table

Feature strum::Display Manual Display
Boilerplate Minimal More code
Variant data access No Yes
Formatter options No Yes
Conditional logic No Yes
Case transformations Built-in Manual
Visibility In enum In impl block
Maintenance Easier More error-prone

Synthesis

strum::Display is ideal for simple variant-to-string mappings:

  • Generates standard Display implementations automatically
  • Keeps output strings visible in the enum definition
  • Provides serialize_all for case transformations
  • Works with EnumString for round-trip parsing
  • Zero runtime overhead compared to manual implementation

Manual Display is necessary when you need:

  • Access to variant data in formatted output
  • Formatter options like {:#} to change behavior
  • Conditional formatting logic
  • Dynamic string construction
  • Access to external data during formatting

Decision guide: If your Display implementation is a simple match that maps variants to static strings, strum::Display reduces boilerplate and keeps the mapping visible. If you need to format variant data, use formatter options, or implement complex logic, write the Display implementation manually. The macro generates the same code you would write by hand for the simple cases—it's purely a convenience for reducing repetitive match expressions.