Rust Enum from a String Union Field
Replace a String field with a typed Rust enum when JSON values come from a fixed set. Compile-time exhaustiveness without giving up serde compatibility.
Detailed Explanation
Promoting a String Field to a serde-Aware Enum
When a JSON field always holds one of a fixed set of strings ("active", "inactive", "banned"), you can replace the generated String field with a Rust enum. The compiler then forces every match arm to be handled.
Example JSON
{
"id": 1,
"name": "Ada",
"status": "active"
}
Generated Rust (initial)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub id: i32,
pub name: String,
pub status: String,
}
Promoted to an enum
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
Active,
Inactive,
Banned,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub id: i32,
pub name: String,
pub status: Status,
}
Why rename_all
Without #[serde(rename_all = "lowercase")], serde would expect the JSON to send "Active" (matching the variant name). Most APIs use lowercase or snake_case, so rename_all keeps the JSON contract intact while letting your Rust code use idiomatic PascalCase variants.
Other rename strategies
| Attribute | JSON form |
|---|---|
"lowercase" |
active |
"UPPERCASE" |
ACTIVE |
"snake_case" |
active_user |
"SCREAMING_SNAKE_CASE" |
ACTIVE_USER |
"kebab-case" |
active-user |
Variant aliases
If the API sends multiple spellings for the same logical value, use per-variant rename:
pub enum Role {
#[serde(rename = "admin")]
Administrator,
#[serde(rename = "user", alias = "regular")]
Regular,
}
Catching unknown values
Add a fallback variant to gracefully handle new server-side values:
#[serde(other)]
Unknown,
This prevents your client from crashing when the API adds a new status next quarter.
Use Case
Order states, user roles, payment statuses, and event types are all natural enums. Rust's exhaustive match plus serde's rename attributes give you compile-time safety with zero runtime overhead.