How does strum::Display derive std::fmt::Display for enum variants with custom representations?

strum::Display generates a std::fmt::Display implementation for enums that by default outputs the variant name as-is, but can be customized using attributes like #[strum(to_string = "...")] or #[strum(serialize_all = "...")] to produce arbitrary string representations without manual Display implementation. This eliminates boilerplate code for enums where each variant needs a specific string representation, while maintaining type safety and supporting runtime string conversion through the same derive.

Basic Display Derivation

use strum::Display;
 
// Deriving Display produces variant names as output
#[derive(Display)]
enum Color {
    Red,
    Green,
    Blue,
}
 
fn basic_usage() {
    let red = Color::Red;
    println!("{}", red);  // Output: Red
    
    let green = Color::Green;
    assert_eq!(format!("{}", green), "Green");
    
    // Each variant's name becomes its Display representation
    assert_eq!(Color::Blue.to_string(), "Blue");
}

The default implementation outputs the variant name exactly as defined.

Custom String Representations

use strum::Display;
 
#[derive(Display)]
enum Status {
    #[strum(to_string = "In Progress")]
    InProgress,
    
    #[strum(to_string = "Completed Successfully")]
    Completed,
    
    #[strum(to_string = "Failed")]
    Failed,
    
    #[strum(to_string = "Pending Review")]
    PendingReview,
}
 
fn custom_strings() {
    let status = Status::InProgress;
    assert_eq!(status.to_string(), "In Progress");
    
    let completed = Status::Completed;
    println!("Status: {}", completed);  // Status: Completed Successfully
    
    // Spaces and special characters are allowed in custom strings
    assert_eq!(Status::PendingReview.to_string(), "Pending Review");
}

to_string attribute overrides the default variant name output.

Case Transformation with serialize_all

use strum::Display;
 
#[derive(Display)]
#[strum(serialize_all = "snake_case")]
enum HttpMethod {
    Get,
    Post,
    Put,
    Delete,
    Patch,
}
 
fn case_transformation() {
    assert_eq!(HttpMethod::Get.to_string(), "get");
    assert_eq!(HttpMethod::Post.to_string(), "post");
    assert_eq!(HttpMethod::Put.to_string(), "put");
    assert_eq!(HttpMethod::Delete.to_string(), "delete");
    
    // The enum-level attribute transforms all variants
}
 
#[derive(Display)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
enum LogLevel {
    Debug,
    Info,
    Warning,
    Error,
}
 
fn screaming_case() {
    assert_eq!(LogLevel::Debug.to_string(), "DEBUG");
    assert_eq!(LogLevel::Warning.to_string(), "WARNING");
    assert_eq!(LogLevel::Error.to_string(), "ERROR");
}

serialize_all applies case transformations to all variants uniformly.

Available Case Transformations

use strum::Display;
 
// snake_case: lower_case_with_underscores
#[derive(Display)]
#[strum(serialize_all = "snake_case")]
enum SnakeCase {
    FirstVariant,   // first_variant
    SecondVariant,  // second_variant
}
 
// SCREAMING_SNAKE_CASE: UPPER_CASE_WITH_UNDERSCORES
#[derive(Display)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
enum ScreamingSnake {
    FirstVariant,   // FIRST_VARIANT
    SecondVariant,  // SECOND_VARIANT
}
 
// kebab_case: lower-case-with-dashes
#[derive(Display)]
#[strum(serialize_all = "kebab-case")]
enum KebabCase {
    FirstVariant,   // first-variant
    SecondVariant,  // second-variant
}
 
// camelCase: lowerCamelCase
#[derive(Display)]
#[strum(serialize_all = "camelCase")]
enum CamelCase {
    FirstVariant,   // firstVariant
    SecondVariant,  // secondVariant
}
 
// PascalCase: UpperCamelCase (the default)
#[derive(Display)]
#[strum(serialize_all = "PascalCase")]
enum PascalCase {
    FirstVariant,   // FirstVariant
    SecondVariant,  // SecondVariant
}
 
// lowercase: all lower case
#[derive(Display)]
#[strum(serialize_all = "lowercase")]
enum LowerCase {
    FirstVariant,   // firstvariant
    SecondVariant,  // secondvariant
}
 
// UPPERCASE: ALL UPPER CASE
#[derive(Display)]
#[strum(serialize_all = "UPPERCASE")]
enum UpperCase {
    FirstVariant,   // FIRSTVARIANT
    SecondVariant,  // SECONDVARIANT
}

Multiple case transformations are available for consistent formatting.

Per-Variant Overrides

use strum::Display;
 
#[derive(Display)]
#[strum(serialize_all = "snake_case")]
enum HttpStatus {
    Ok,                // ok (uses serialize_all)
    
    #[strum(to_string = "created")]
    Created,            // created (same as default but explicit)
    
    #[strum(to_string = "bad_request")]
    BadRequest,         // bad_request
    
    #[strum(to_string = "Internal Server Error")]
    InternalServerError,  // Internal Server Error (overrides serialize_all)
    
    #[strum(to_string = "Service Unavailable")]
    ServiceUnavailable,    // Service Unavailable
}
 
fn per_variant() {
    assert_eq!(HttpStatus::Ok.to_string(), "ok");
    assert_eq!(HttpStatus::BadRequest.to_string(), "bad_request");
    assert_eq!(HttpStatus::InternalServerError.to_string(), "Internal Server Error");
    
    // Per-variant to_string overrides the enum-level serialize_all
}

Variant-level to_string overrides the enum-level serialize_all setting.

Variants with Fields

use strum::Display;
 
#[derive(Display)]
enum Message {
    Simple,
    
    // Named fields - Display ignores them by default
    WithFields { code: i32, text: String },
    
    // Tuple variants - Display ignores fields
    Code(i32),
    
    // Custom string for variants with fields
    #[strum(to_string = "Error occurred")]
    Error { code: i32 },
}
 
fn with_fields() {
    let simple = Message::Simple;
    assert_eq!(simple.to_string(), "Simple");
    
    // Named fields: Display shows variant name, fields are ignored
    let with_fields = Message::WithFields { code: 404, text: "Not Found".into() };
    assert_eq!(with_fields.to_string(), "WithFields");
    
    // Tuple variants: same behavior
    let code = Message::Code(500);
    assert_eq!(code.to_string(), "Code");
    
    // Custom string takes precedence
    let error = Message::Error { code: 500 };
    assert_eq!(error.to_string(), "Error occurred");
    
    // Note: strum::Display doesn't include field values
    // To include fields, implement Display manually
}

Display ignores variant fields by default; only the variant name is used.

Manual Display for Field Values

use std::fmt;
use strum::Display;
 
// If you need field values in Display, implement manually
#[derive(Display)]
enum StatusWithCode {
    #[strum(to_string = "OK")]
    Ok,
    
    #[strum(to_string = "Error")]
    Error,
}
 
// For custom field formatting, implement Display manually
enum DetailedStatus {
    Ok,
    Error { code: i32, message: String },
}
 
impl fmt::Display for DetailedStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DetailedStatus::Ok => write!(f, "OK"),
            DetailedStatus::Error { code, message } => {
                write!(f, "Error {}: {}", code, message)
            }
        }
    }
}
 
fn manual_display() {
    let status = DetailedStatus::Error {
        code: 500,
        message: "Internal Server Error".into(),
    };
    assert_eq!(status.to_string(), "Error 500: Internal Server Error");
    
    // strum::Display is for fixed strings per variant
    // Manual Display is for dynamic content with fields
}

For dynamic formatting with field values, implement Display manually.

Combining with Other Strum Derives

use strum::{Display, EnumString, IntoStaticStr};
 
#[derive(Display, EnumString, IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
enum Direction {
    North,
    South,
    East,
    West,
}
 
fn combined_derives() {
    // Display: to_string()
    let dir = Direction::North;
    assert_eq!(dir.to_string(), "north");
    
    // EnumString: FromStr
    let parsed: Direction = "north".parse().unwrap();
    assert!(matches!(parsed, Direction::North));
    
    // IntoStaticStr: &'static str
    let static_str: &'static str = Direction::North.into();
    assert_eq!(static_str, "north");
    
    // All three derives can work together
    // serialize_all applies to all string conversions
}

Display works with other strum derives for comprehensive string handling.

Using IntoStaticStr vs Display

use strum::{Display, IntoStaticStr};
 
#[derive(Display, IntoStaticStr)]
#[strum(serialize_all = "kebab-case")]
enum Format {
    Json,
    Xml,
    Yaml,
}
 
fn compare_derives() {
    // Display: returns String (allocates)
    let format = Format::Json;
    let display_string: String = format.to_string();
    assert_eq!(display_string, "json");
    
    // IntoStaticStr: returns &'static str (no allocation)
    let static_str: &'static str = Format::Json.into();
    assert_eq!(static_str, "json");
    
    // Use IntoStaticStr when you need a static string:
    // - Match patterns
    // - const contexts
    // - Function parameters accepting &'static str
    
    // Use Display when:
    // - You need String or fmt::Display
    // - Working with format!() macros
    // - Printing to users
}

IntoStaticStr provides &'static str; Display provides String via to_string().

Default Values with strum(default)

use strum::{Display, EnumString};
 
#[derive(Display, EnumString)]
enum Size {
    Small,
    Medium,
    Large,
    
    #[strum(default)]
    Unknown,  // Fallback for unrecognized strings
}
 
fn default_variant() {
    // Display works normally
    assert_eq!(Size::Small.to_string(), "Small");
    assert_eq!(Size::Unknown.to_string(), "Unknown");
    
    // EnumString uses default for unknown strings
    let unknown: Size = "extra-large".parse().unwrap();
    assert!(matches!(unknown, Size::Unknown));
    
    // Note: default affects EnumString parsing, not Display
}

The default attribute applies to parsing, not Display output.

Advanced Customization

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,
}
 
fn emoji_strings() {
    assert_eq!(Priority::High.to_string(), "🔴 High Priority");
    assert_eq!(Priority::Medium.to_string(), "🟡 Medium Priority");
    assert_eq!(Priority::Low.to_string(), "🟢 Low Priority");
    
    // Unicode and special characters work in to_string
}
 
// Multiple string representations using strum's other features
#[derive(Display)]
#[strum(serialize_all = "UPPERCASE")]
enum ErrorCode {
    #[strum(to_string = "ERR_001")]
    ConnectionTimeout,
    
    #[strum(to_string = "ERR_002")]
    AuthenticationFailed,
    
    #[strum(to_string = "ERR_003")]
    ResourceNotFound,
    
    InternalError,  // INTERNALERROR (from serialize_all)
}
 
fn mixed_formats() {
    assert_eq!(ErrorCode::ConnectionTimeout.to_string(), "ERR_001");
    assert_eq!(ErrorCode::InternalError.to_string(), "INTERNALERROR");
    
    // Mix explicit to_string with serialize_all defaults
}

Custom strings can include Unicode and arbitrary content.

Practical Use Cases

use strum::Display;
 
// User-facing messages
#[derive(Display)]
#[strum(serialize_all = "lowercase")]
enum UserMessage {
    Welcome,
    Goodbye,
    #[strum(to_string = "please wait...")]
    Loading,
    #[strum(to_string = "operation completed successfully")]
    Success,
}
 
fn user_messages() {
    println!("{}", UserMessage::Welcome);    // welcome
    println!("{}", UserMessage::Loading);    // please wait...
    println!("{}", UserMessage::Success);    // operation completed successfully
}
 
// HTTP headers
#[derive(Display)]
#[strum(serialize_all = "PascalCase")]
enum HttpHeader {
    ContentType,
    ContentLength,
    #[strum(to_string = "X-Custom-Header")]
    CustomHeader,
    Authorization,
}
 
fn http_headers() {
    assert_eq!(HttpHeader::ContentType.to_string(), "ContentType");
    // Note: HTTP headers typically need Title-Case, customize as needed
}
 
// Configuration values
#[derive(Display)]
#[strum(serialize_all = "kebab-case")]
enum ConfigKey {
    DatabaseUrl,
    MaxConnections,
    #[strum(to_string = "timeout-secs")]
    TimeoutSeconds,
}
 
fn config_keys() {
    assert_eq!(ConfigKey::DatabaseUrl.to_string(), "database-url");
    assert_eq!(ConfigKey::MaxConnections.to_string(), "max-connections");
    assert_eq!(ConfigKey::TimeoutSeconds.to_string(), "timeout-secs");
}

strum::Display simplifies consistent string representations across the application.

Generated Code

use strum::Display;
 
// When you write:
#[derive(Display)]
#[strum(serialize_all = "snake_case")]
enum Example {
    First,
    Second,
    Third,
}
 
// strum generates something like:
impl std::fmt::Display for Example {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Example::First => write!(f, "first"),
            Example::Second => write!(f, "second"),
            Example::Third => write!(f, "third"),
        }
    }
}
 
// The generated code is straightforward and efficient
// Each variant maps to its string representation

The generated implementation is a simple match expression with write!.

Synthesis

Quick reference:

Attribute Level Purpose
#[strum(to_string = "...")] Variant Custom string for that variant
#[strum(serialize_all = "...")] Enum Case transformation for all variants

Case transformations:

Style Example Input Output
snake_case MyVariant my_variant
SCREAMING_SNAKE_CASE MyVariant MY_VARIANT
kebab-case MyVariant my-variant
camelCase MyVariant myVariant
PascalCase my_variant MyVariant
lowercase MyVariant myvariant
UPPERCASE my_variant MYVARIANT

Key insight: strum::Display derives std::fmt::Display for enums by generating a match expression that maps each variant to a string representation. By default, it uses the variant name as-is (PascalCase). The serialize_all attribute applies case transformations to all variants uniformly—snake_case produces lower_case, SCREAMING_SNAKE_CASE produces UPPER_CASE, kebab-case produces lower-kebab, and so on. Per-variant to_string attributes override the global serialize_all setting for individual variants, enabling mixed formatting when needed. The derive works with tuple and struct variants but ignores their fields—the Display output is purely based on the variant name or custom string, not field contents. For dynamic formatting that includes field values, implement Display manually. Combine strum::Display with EnumString for bidirectional string conversion (parsing and formatting), and IntoStaticStr for zero-allocation string references. The generated code is a simple match expression with write! calls, making it as efficient as hand-written implementations while eliminating boilerplate. Use strum::Display when you need consistent, configurable string representations for enum variants without writing repetitive match expressions.