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 stringstrum::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 approachstrum::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 producedstrum::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 implementationstrum::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
Displayimplementations automatically - Keeps output strings visible in the enum definition
- Provides
serialize_allfor case transformations - Works with
EnumStringfor 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.
