When enums need to talk to strings
You build an enum for configuration modes. You need to print the mode name to a log file. You also need to parse a string from a config file back into the mode. You write a match statement for printing. You write another match for parsing. You add a new variant later. You update the print match. You forget the parse match. The code compiles. The app crashes at runtime when it encounters the new mode.
Enums are compile-time guarantees. Strings are runtime chaos. Manual conversions drift. The strum crate ties string representations directly to the enum definition. If you add a variant, the generated code updates automatically. You get iteration, parsing, and display without the boilerplate.
The derive macro approach
strum provides derive macros that generate trait implementations for your enums. You annotate the enum with #[derive(...)]. The macro runs at compile time, inspects the variants, and generates the code. There is no runtime cost for the generation. The output is standard Rust code.
The crate covers the common patterns: converting to strings, parsing from strings, iterating over variants, and handling serialization formats. It also includes advanced features like generating discriminant enums for enums that carry data.
Minimal example
Add strum to your dependencies. The crate re-exports the macros, so you only need one dependency.
[dependencies]
strum = "0.26"
Derive Display to print variants and EnumIter to loop over them.
use strum_macros::{Display, EnumIter};
#[derive(Display, EnumIter)]
enum Color {
Red,
Blue,
Green,
}
fn main() {
// Display generates the fmt::Display impl.
// This prints "Red" without manual formatting.
println!("{}", Color::Red);
// EnumIter creates a static iterator over all variants.
// The order matches the declaration order in the enum.
for color in Color::iter() {
println!("Found: {}", color);
}
}
The Display trait lets you use {} in format strings. EnumIter provides a iter() method that yields each variant. You can validate input by checking if a string exists in the iterator.
Convention aside: use strum_macros only if you need a specific version mismatch for some reason. The strum crate re-exports all macros. The community standard is to depend on strum and import from strum_macros or strum directly. This keeps versions aligned and avoids subtle breakage.
Tie the string to the variant. Drift is the enemy.
Realistic example: CLI commands
Command-line tools often parse subcommands from strings and display help text. strum handles both directions. EnumString enables parsing. Display handles output. serialize_all controls the string format.
use strum_macros::{Display, EnumString, EnumIter};
#[derive(Debug, Display, EnumString, EnumIter)]
#[strum(serialize_all = "kebab-case")]
enum Command {
Start,
Stop,
StatusCheck,
ListUsers,
}
fn main() {
let input = "status-check";
// EnumString implements FromStr.
// parse() returns a Result, handling invalid input gracefully.
let cmd: Command = match input.parse() {
Ok(c) => c,
Err(_) => {
eprintln!("Unknown command: {}", input);
return;
}
};
println!("Executing: {:?}", cmd);
// Generate help text by iterating variants.
// serialize_all ensures the help text matches the input format.
println!("Available commands:");
for c in Command::iter() {
println!(" - {}", c);
}
}
The #[strum(serialize_all = "kebab-case")] attribute applies to all variants. StatusCheck becomes status-check. This saves manual #[strum(serialize = "...")] on every variant. Supported formats include lowercase, UPPERCASE, PascalCase, camelCase, snake_case, kebab-case, SCREAMING_SNAKE_CASE, and SCREAMING-KEBAB-CASE.
If you forget EnumString and try to parse, the compiler rejects the code with E0277 (the trait bound Command: FromStr is not satisfied). The error points directly to the missing derive.
Use serialize_all to enforce consistency. Manual serialization attributes are maintenance debt.
Advanced: Discriminants for enums with data
Enums often carry data. Display and EnumString struggle with fields because the string representation is ambiguous. strum solves this with EnumDiscriminants. It generates a new enum containing only the variant names, stripping the data. You can derive traits on the discriminant enum separately.
use strum_macros::{EnumDiscriminants, Display};
#[derive(EnumDiscriminants)]
#[strum_discriminants(derive(Display))]
enum Message {
Hello { id: i32 },
Goodbye { reason: String },
Data { payload: Vec<u8> },
}
fn main() {
let msg = Message::Hello { id: 42 };
// Convert to the discriminant enum to inspect the variant type.
let discriminant = MessageDiscriminants::from(&msg);
// Display works on the discriminant, ignoring the data.
println!("Message type: {}", discriminant);
// Match on the discriminant for routing logic.
match discriminant {
MessageDiscriminants::Hello => println!("Handling hello"),
MessageDiscriminants::Goodbye => println!("Handling goodbye"),
MessageDiscriminants::Data => println!("Handling data"),
}
}
The macro generates MessageDiscriminants. The #[strum_discriminants(derive(Display))] attribute applies derives to the generated enum. This pattern is useful for routing, logging, or serialization where you need the variant name but not the payload.
Convention aside: EnumDiscriminants is often combined with EnumIter on the discriminant to iterate over variant types. This lets you validate that a message type is supported before processing the payload.
Generate the discriminant. Match the shape, ignore the payload.
Performance: AsRefStr for comparisons
Display produces a String via to_string(). This allocates memory. If you only need a string slice for comparisons or map keys, use AsRefStr. It returns &str without allocation.
use strum_macros::{AsRefStr, EnumString};
#[derive(AsRefStr, EnumString)]
enum Status {
Active,
Pending,
Failed,
}
fn main() {
let status = Status::Active;
// AsRefStr provides as_ref() which returns &str.
// No allocation occurs.
let name: &str = status.as_ref();
println!("Status: {}", name);
// Use as_ref() for efficient comparisons.
if status.as_ref() == "Active" {
println!("System is running");
}
}
AsRefStr is faster than Display for comparisons. It avoids heap allocation. Use it for HashMap keys, database lookups, or any hot path where you compare enum variants to strings.
Use AsRefStr for comparisons. Avoid the allocation.
Pitfalls and errors
Enums with fields break simple string conversion. If you derive Display on an enum with data, strum requires explicit formatting.
use strum_macros::Display;
#[derive(Display)]
enum Result {
#[strum(to_string = "Success")]
Ok,
#[strum(to_string = "Error: {0}")]
Err(String),
}
fn main() {
println!("{}", Result::Err("timeout".to_string()));
}
The #[strum(to_string = "...")] attribute defines the format. Without it, the macro fails to generate the implementation. You get a compiler error about missing methods.
Parsing returns a Result. Unwrapping panics on invalid input. Handle the error explicitly.
let input = "invalid";
let cmd: Command = input.parse().expect("Valid command required");
expect panics with a message. In production code, return the error or provide a default.
Version mismatches cause cryptic errors. strum and strum_macros must share the same major version. Since strum re-exports macros, depending only on strum prevents drift.
Keep strum and strum_macros versions locked together. Mismatched versions break the macro expansion silently or with cryptic errors.
Decision matrix
Use strum when you need to convert enum variants to and from strings without writing manual match arms. Use strum when you need to iterate over all variants of an enum for validation or help text. Use strum when you want to enforce serialization formats like kebab-case or snake-case across an entire enum. Use strum when you need to generate a discriminant enum for variants that carry data. Reach for manual match when your enum variants carry data that requires complex formatting logic beyond simple string interpolation. Reach for serde when you are serializing to JSON or other data formats and need full ecosystem integration.
Pick the tool that matches the job. strum handles the string glue. serde handles the data interchange.