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:
- Display β convert enum variants to strings
- FromStr / EnumString β parse strings to enum variants
- EnumIter β iterate over all enum variants
- IntoStaticStr β convert to
&'static str - 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
DisplayderivesToStringfor enum variantsEnumStringderivesFromStrfor parsing strings to enumsEnumIterenables iteration over all variants viaiter()IntoStaticStrconverts to&'static strEnumVariantNamesprovidesVARIANTSconstant with all namesEnumCountprovidesCOUNTconstant with variant countFromReprconverts discriminants to enum variantsEnumMessageaddsget_message()andget_detailed_message()EnumPropertyaddsget_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 strumcrate contains traits,strum_macroscontains derives- Perfect for: configuration enums, state machines, CLI options, API endpoints, validation
