How do I derive enum utilities with strum in Rust?

Walkthrough

The strum crate provides derive macros that add useful functionality to Rust enums. Instead of manually implementing FromStr, Display, iteration, and other traits for your enums, strum generates this boilerplate automatically. The related strum_macros crate contains the derive implementations. Strum makes working with enums more ergonomic by providing string conversions, iteration over variants, property attachment, and moreβ€”all through simple derive attributes.

Key concepts:

  1. Display β€” convert enum variants to strings
  2. FromStr / EnumString β€” parse strings to enum variants
  3. EnumIter β€” iterate over all enum variants
  4. IntoStaticStr β€” convert to &'static str
  5. EnumVariantNames β€” get variant names as constants

Code Example

# Cargo.toml
[dependencies]
strum = "0.26"
strum_macros = "0.26"
use strum::IntoEnumIterator;
use strum_macros::{Display, EnumString, EnumIter};
 
#[derive(Debug, Display, EnumString, EnumIter)]
enum Color {
    Red,
    Green,
    Blue,
}
 
fn main() {
    // Display
    println!("Color: {}", Color::Red);  // "Color: Red"
    
    // FromStr
    let color: Color = "Green".parse().unwrap();
    println!("Parsed: {:?}", color);
    
    // Iterate
    for color in Color::iter() {
        println!("Variant: {}", color);
    }
}

Basic String Conversion

use strum_macros::{Display, EnumString};
 
#[derive(Debug, Display, EnumString)]
enum Status {
    Active,
    Inactive,
    Pending,
}
 
fn main() {
    // Display trait - enum to string
    let status = Status::Active;
    println!("Status: {}", status);  // "Status: Active"
    
    let status_str = status.to_string();
    println!("As string: {}", status_str);
    
    // EnumString trait - string to enum
    let parsed: Status = "Inactive".parse().unwrap();
    println!("Parsed: {:?}", parsed);
    
    // Handle parse errors
    let result: Result<Status, _> = "Unknown".parse();
    println!("Parse result: {:?}", result);
}

Custom String Representations

use strum_macros::{Display, EnumString};
 
#[derive(Debug, Display, EnumString)]
enum Role {
    #[strum(to_string = "admin")]
    Admin,
    
    #[strum(to_string = "moderator")]
    Moderator,
    
    #[strum(to_string = "regular_user")]
    RegularUser,
}
 
fn main() {
    let role = Role::Admin;
    println!("Role: {}", role);  // "Role: admin"
    
    // Parse with custom string
    let parsed: Role = "moderator".parse().unwrap();
    println!("Parsed: {:?}", parsed);
    
    // Case-sensitive parsing
    let result: Result<Role, _> = "Admin".parse();
    println!("Case sensitive: {:?}", result);  // Err
}

Case-Insensitive Parsing

use strum_macros::EnumString;
 
#[derive(Debug, EnumString)]
enum HttpMethod {
    #[strum(ascii_case_insensitive)]
    Get,
    
    #[strum(ascii_case_insensitive)]
    Post,
    
    #[strum(ascii_case_insensitive)]
    Put,
    
    #[strum(ascii_case_insensitive)]
    Delete,
}
 
fn main() {
    // All these work with case-insensitive parsing
    let get1: HttpMethod = "GET".parse().unwrap();
    let get2: HttpMethod = "get".parse().unwrap();
    let get3: HttpMethod = "Get".parse().unwrap();
    
    println!("All parsed: {:?}, {:?}, {:?}", get1, get2, get3);
}

Enum Iteration

use strum::IntoEnumIterator;
use strum_macros::EnumIter;
 
#[derive(Debug, EnumIter)]
enum Direction {
    North,
    South,
    East,
    West,
}
 
fn main() {
    // Iterate over all variants
    println!("All directions:");
    for dir in Direction::iter() {
        println!("  - {:?}", dir);
    }
    
    // Collect into vector
    let all_dirs: Vec<Direction> = Direction::iter().collect();
    println!("Count: {}", all_dirs.len());
    
    // Use with iterator methods
    let count = Direction::iter().count();
    println!("Total variants: {}", count);
}

Into Static Str

use strum_macros::IntoStaticStr;
 
#[derive(Debug, IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
enum EventType {
    UserCreated,
    UserDeleted,
    OrderPlaced,
    OrderShipped,
}
 
fn main() {
    // Convert to &'static str
    let event: EventType = EventType::UserCreated;
    let name: &'static str = event.into();
    
    println!("Event name: {}", name);  // "user_created"
    
    // Use in static contexts
    fn get_event_name(event: EventType) -> &'static str {
        event.into()
    }
    
    println!("Name: {}", get_event_name(EventType::OrderPlaced));
}

Serialize All Variants

use strum_macros::{Display, EnumString};
 
#[derive(Debug, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
enum ErrorCode {
    InvalidInput,
    NetworkError,
    DatabaseFailure,
    AuthenticationFailed,
}
 
fn main() {
    // All variants use snake_case
    let error = ErrorCode::InvalidInput;
    println!("Error: {}", error);  // "Error: invalid_input"
    
    // Parse with snake_case
    let parsed: ErrorCode = "network_error".parse().unwrap();
    println!("Parsed: {:?}", parsed);
    
    // Available serialize_all options:
    // - "lowercase"
    // - "UPPERCASE"
    // - "snake_case"
    // - "kebab-case"
    // - "camelCase"
    // - "PascalCase"
    // - "SCREAMING_SNAKE_CASE"
}

Variant Names

use strum_macros::EnumVariantNames;
 
#[derive(Debug, EnumVariantNames)]
enum Animal {
    Dog,
    Cat,
    Bird,
    Fish,
}
 
fn main() {
    // Get all variant names as a slice
    let names: &[&str] = Animal::VARIANTS;
    println!("Variant names: {:?}", names);
    
    // Use in validation
    fn is_valid_animal(name: &str) -> bool {
        Animal::VARIANTS.contains(&name)
    }
    
    println!("Dog valid: {}", is_valid_animal("Dog"));
    println!("Cow valid: {}", is_valid_animal("Cow"));
}

Enum Count

use strum_macros::EnumCount;
 
#[derive(Debug, EnumCount)]
enum Priority {
    Low,
    Medium,
    High,
    Critical,
}
 
fn main() {
    // Get the count at compile time
    println!("Priority count: {}", Priority::COUNT);
    
    // Useful for array sizing
    let priority_counts = [0u32; Priority::COUNT];
    println!("Array length: {}", priority_counts.len());
}

Enum Discriminant

use strum::FromRepr;
use strum_macros::FromRepr;
 
#[derive(Debug, FromRepr)]
#[repr(u8)]
enum Command {
    Start = 1,
    Stop = 2,
    Pause = 3,
    Resume = 4,
}
 
fn main() {
    // Convert from discriminant
    let cmd = Command::from_repr(1);
    println!("Command 1: {:?}", cmd);  // Some(Start)
    
    let invalid = Command::from_repr(99);
    println!("Command 99: {:?}", invalid);  // None
    
    // Useful for parsing binary protocols
    fn parse_command(byte: u8) -> Option<Command> {
        Command::from_repr(byte)
    }
}

Enum Messages

use strum_macros::EnumMessage;
 
#[derive(Debug, EnumMessage)]
enum LogLevel {
    #[strum(message = "Detailed debug information")]
    Debug,
    
    #[strum(message = "General information")]
    Info,
    
    #[strum(message = "Warning about potential issues")]
    Warning,
    
    #[strum(message = "Error that needs attention")]
    Error,
}
 
fn main() {
    let level = LogLevel::Warning;
    
    if let Some(msg) = level.get_message() {
        println!("Level: {:?} - {}", level, msg);
    }
    
    // Iterate with messages
    use strum::IntoEnumIterator;
    #[derive(EnumIter, EnumMessage)]
    enum Priority {
        #[strum(message = "Low priority")]
        Low,
        #[strum(message = "High priority")]
        High,
    }
    
    for priority in Priority::iter() {
        println!("{:?}: {}", priority, priority.get_message().unwrap());
    }
}

Detailed Documentation

use strum_macros::EnumMessage;
 
#[derive(Debug, EnumMessage)]
enum ConfigOption {
    #[strum(
        message = "Server port",
        detailed_message = "The TCP port the server will listen on. Default: 8080"
    )]
    Port,
    
    #[strum(
        message = "Database URL",
        detailed_message = "Connection string for the database. Required for production."
    )]
    DatabaseUrl,
    
    #[strum(
        message = "Log level",
        detailed_message = "Verbosity of logging: debug, info, warn, error"
    )]
    LogLevel,
}
 
fn main() {
    let opt = ConfigOption::Port;
    
    println!("Short: {}", opt.get_message().unwrap());
    println!("Detailed: {}", opt.get_detailed_message().unwrap());
}

Enum Properties

use strum_macros::EnumProperty;
 
#[derive(Debug, EnumProperty)]
enum HttpStatus {
    #[strum(props(code = "200", description = "OK"))]
    Ok,
    
    #[strum(props(code = "201", description = "Created"))]
    Created,
    
    #[strum(props(code = "400", description = "Bad Request"))]
    BadRequest,
    
    #[strum(props(code = "404", description = "Not Found"))]
    NotFound,
    
    #[strum(props(code = "500", description = "Internal Server Error"))]
    InternalError,
}
 
fn main() {
    let status = HttpStatus::NotFound;
    
    println!("Code: {:?}", status.get_str("code"));
    println!("Description: {:?}", status.get_str("description"));
    
    // Convert to integer
    fn get_status_code(status: HttpStatus) -> u16 {
        status.get_str("code")
            .and_then(|s| s.parse().ok())
            .unwrap()
    }
    
    println!("Numeric code: {}", get_status_code(HttpStatus::Ok));
}

Multiple Derives Combined

use strum::IntoEnumIterator;
use strum_macros::{Display, EnumString, EnumIter, EnumMessage, EnumProperty};
 
#[derive(Debug, Clone, Display, EnumString, EnumIter, EnumMessage, EnumProperty)]
#[strum(serialize_all = "kebab-case")]
enum Feature {
    #[strum(message = "User authentication", props(priority = "high"))]
    Authentication,
    
    #[strum(message = "Data export", props(priority = "medium"))]
    Export,
    
    #[strum(message = "Real-time notifications", props(priority = "low"))]
    Notifications,
}
 
fn main() {
    // Display
    println!("Feature: {}", Feature::Authentication);
    
    // Parse
    let feature: Feature = "export".parse().unwrap();
    println!("Parsed: {:?}", feature);
    
    // Iterate with messages and properties
    for f in Feature::iter() {
        println!(
            "{}: {} (priority: {:?})",
            f,
            f.get_message().unwrap(),
            f.get_str("priority")
        );
    }
}

Enums with Data

use strum_macros::{Display, EnumString};
 
#[derive(Debug, Display, EnumString)]
enum Result<T, E> {
    Ok(T),
    Err(E),
}
 
// Better approach: use variants with specific meanings
#[derive(Debug, Display, EnumString)]
enum Status {
    #[strum(to_string = "success")]
    Success,
    
    #[strum(to_string = "failure")]
    Failure,
    
    #[strum(to_string = "pending")]
    Pending,
}
 
fn main() {
    let status = Status::Success;
    println!("Status: {}", status);
}

Skip Variants

use strum::IntoEnumIterator;
use strum_macros::{Display, EnumIter, EnumString};
 
#[derive(Debug, Display, EnumIter, EnumString)]
enum Config {
    Host,
    Port,
    
    #[strum(disabled)]
    Internal,  // Won't be included in iter() or parsing
}
 
fn main() {
    // Internal is skipped in iteration
    for config in Config::iter() {
        println!("Config: {}", config);
    }
    
    // Internal cannot be parsed
    let result: Result<Config, _> = "Internal".parse();
    println!("Parse Internal: {:?}", result);  // Err
}

Default Variant

use strum_macros::EnumString;
 
#[derive(Debug, EnumString)]
enum Color {
    Red,
    Green,
    Blue,
    
    #[strum(default)]
    Unknown(String),  // Catches any unrecognized value
}
 
fn main() {
    let known: Color = "Red".parse().unwrap();
    println!("Known: {:?}", known);
    
    let unknown: Color = "Purple".parse().unwrap();
    println!("Unknown: {:?}", unknown);  // Unknown("Purple")
}

Using with Serde

use strum_macros::{Display, EnumString};
use serde::{Deserialize, Serialize};
 
#[derive(Debug, Clone, Display, EnumString, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
enum Action {
    CreateUser,
    DeleteUser,
    UpdateUser,
    ListUsers,
}
 
fn main() {
    let action = Action::CreateUser;
    
    // Serde JSON serialization
    let json = serde_json::to_string(&action).unwrap();
    println!("JSON: {}", json);  // "create_user"
    
    // Display
    println!("Display: {}", action);  // "create_user"
    
    // Parse
    let parsed: Action = "delete_user".parse().unwrap();
    println!("Parsed: {:?}", parsed);
}

Building CLI Arguments

use strum::IntoEnumIterator;
use strum_macros::{Display, EnumString, EnumIter};
 
#[derive(Debug, Clone, Display, EnumString, EnumIter)]
enum OutputFormat {
    Json,
    Yaml,
    Toml,
    Text,
}
 
impl OutputFormat {
    fn extensions(&self) -> &'static str {
        match self {
            OutputFormat::Json => ".json",
            OutputFormat::Yaml => ".yaml,.yml",
            OutputFormat::Toml => ".toml",
            OutputFormat::Text => ".txt",
        }
    }
}
 
fn main() {
    // List available formats
    println!("Available formats:");
    for format in OutputFormat::iter() {
        println!("  {} ({})", format, format.extensions());
    }
    
    // Parse user input
    let user_choice = "yaml";
    let format: OutputFormat = user_choice.parse().unwrap();
    println!("Selected: {}", format);
}

Configuration Enum Pattern

use strum::IntoEnumIterator;
use strum_macros::{Display, EnumIter, EnumMessage};
 
#[derive(Debug, Clone, Display, EnumIter, EnumMessage)]
enum LogLevel {
    #[strum(message = "Off - No logging")]
    Off,
    
    #[strum(message = "Error - Only errors")]
    Error,
    
    #[strum(message = "Warn - Warnings and errors")]
    Warn,
    
    #[strum(message = "Info - General information")]
    Info,
    
    #[strum(message = "Debug - Detailed debugging")]
    Debug,
    
    #[strum(message = "Trace - Everything")]
    Trace,
}
 
fn print_log_levels() {
    println!("Log levels:");
    for level in LogLevel::iter() {
        println!("  {:6} - {}", level, level.get_message().unwrap());
    }
}
 
fn main() {
    print_log_levels();
}

Validation with Enum

use strum_macros::{Display, EnumString, EnumVariantNames};
 
#[derive(Debug, Display, EnumString, EnumVariantNames)]
enum Country {
    US,
    UK,
    CA,
    DE,
    FR,
    JP,
    AU,
}
 
fn validate_country(code: &str) -> Result<Country, String> {
    code.parse::<Country>()
        .map_err(|_| {
            format!(
                "Invalid country code '{}'. Valid codes: {:?}",
                code,
                Country::VARIANTS
            )
        })
}
 
fn main() {
    match validate_country("DE") {
        Ok(country) => println!("Valid: {}", country),
        Err(e) => println!("Error: {}", e),
    }
    
    match validate_country("XX") {
        Ok(country) => println!("Valid: {}", country),
        Err(e) => println!("Error: {}", e),
    }
}

Real-World Example: API Endpoints

use strum::IntoEnumIterator;
use strum_macros::{Display, EnumIter, EnumString};
 
#[derive(Debug, Clone, Display, EnumIter, EnumString)]
#[strum(serialize_all = "kebab-case")]
enum Endpoint {
    #[strum(to_string = "users")]
    Users,
    
    #[strum(to_string = "users/{id}")]
    UserById,
    
    #[strum(to_string = "posts")]
    Posts,
    
    #[strum(to_string = "posts/{id}/comments")]
    PostComments,
    
    #[strum(to_string = "auth/login")]
    Login,
}
 
impl Endpoint {
    fn full_path(&self) -> String {
        format!("/api/v1/{}", self)
    }
}
 
fn main() {
    println!("Available endpoints:");
    for endpoint in Endpoint::iter() {
        println!("  {}", endpoint.full_path());
    }
    
    let endpoint: Endpoint = "posts".parse().unwrap();
    println!("Selected: {}", endpoint.full_path());
}

Real-World Example: State Machine

use strum::IntoEnumIterator;
use strum_macros::{Display, EnumIter, FromRepr};
 
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumIter, FromRepr)]
#[repr(u8)]
enum State {
    #[strum(to_string = "idle")]
    Idle = 0,
    
    #[strum(to_string = "connecting")]
    Connecting = 1,
    
    #[strum(to_string = "connected")]
    Connected = 2,
    
    #[strum(to_string = "disconnecting")]
    Disconnecting = 3,
    
    #[strum(to_string = "error")]
    Error = 99,
}
 
impl State {
    fn transitions(&self) -> Vec<State> {
        use State::*;
        match self {
            Idle => vec![Connecting],
            Connecting => vec![Connected, Error],
            Connected => vec![Disconnecting],
            Disconnecting => vec![Idle],
            Error => vec![Idle],
        }
    }
    
    fn can_transition_to(&self, target: State) -> bool {
        self.transitions().contains(&target)
    }
}
 
fn main() {
    println!("State: {}", State::Idle);
    
    // All states
    for state in State::iter() {
        println!("State {} can transition to: {:?}", 
            state, 
            state.transitions()
        );
    }
    
    // From repr
    let state = State::from_repr(2);
    println!("State 2: {:?}", state);
}

Summary

  • Display derives ToString for enum variants
  • EnumString derives FromStr for parsing strings to enums
  • EnumIter enables iteration over all variants via iter()
  • IntoStaticStr converts to &'static str
  • EnumVariantNames provides VARIANTS constant with all names
  • EnumCount provides COUNT constant with variant count
  • FromRepr converts discriminants to enum variants
  • EnumMessage adds get_message() and get_detailed_message()
  • EnumProperty adds get_str() for custom properties
  • Use #[strum(to_string = "...")] for custom serialization
  • Use #[strum(serialize_all = "...")] for case transformation
  • Use #[strum(disabled)] to skip variants
  • Use #[strum(default)] for catch-all variant
  • strum crate contains traits, strum_macros contains derives
  • Perfect for: configuration enums, state machines, CLI options, API endpoints, validation