Introduction
When deserializing JSON into a Rust enum with Serde, any unrecognized variant name causes a hard deserialization error. This is a common problem when APIs evolve: the server sends new enum values that your Rust client does not yet know about. The default behavior rejects the entire payload, which can break backward compatibility.
Symptoms
Error("unknown variantnew_feature, expected one ofold,legacy")- Deserialization fails entirely when a single enum field has an unknown value
- API updates break older client versions
- Works for known variants but fails on new API versions
- Error occurs with externally tagged, internally tagged, and adjacently tagged enums
Example error: ```rust use serde::Deserialize;
#[derive(Deserialize)] enum Status { Active, Inactive, Pending, }
// JSON from API: {"status": "Archived"}
// Error: unknown variant Archived, expected Active, Inactive, or Pending
```
Common Causes
- API adds new enum values without client update
- Client code not updated for all server-side enum variants
- Third-party API documentation does not list all possible values
- Testing against newer API version than the client supports
- Case sensitivity mismatch between JSON and Rust enum variants
Step-by-Step Fix
- 1.Add a catch-all variant with serde other attribute:
- 2.```rust
- 3.use serde::Deserialize;
#[derive(Deserialize, Debug)] #[serde(rename_all = "snake_case")] enum Status { Active, Inactive, Pending, #[serde(other)] Unknown, }
// Now {"status": "archived"} deserializes to Status::Unknown ```
- 1.**Use
#[serde(alias)]for known alternative names**: - 2.```rust
- 3.#[derive(Deserialize, Debug)]
- 4.enum Status {
- 5.Active,
- 6.#[serde(alias = "disabled", alias = "deactivated")]
- 7.Inactive,
- 8.Pending,
- 9.}
- 10.
` - 11.Handle unknown variants with a custom deserializer:
- 12.```rust
- 13.use serde::{Deserialize, Deserializer};
#[derive(Debug)] enum Status { Active, Inactive, Pending, Unknown(String), }
impl<'de> Deserialize<'de> for Status { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; match s.as_str() { "active" => Ok(Status::Active), "inactive" => Ok(Status::Inactive), "pending" => Ok(Status::Pending), other => Ok(Status::Unknown(other.to_string())), } } } ```
- 1.Use Option to tolerate missing or unknown values:
- 2.```rust
- 3.#[derive(Deserialize)]
- 4.struct ApiResponse {
- 5.#[serde(deserialize_with = "deserialize_status_or_none")]
- 6.status: Option<Status>,
- 7.}
fn deserialize_status_or_none<'de, D>(deserializer: D) -> Result<Option<Status>, D::Error> where D: serde::Deserializer<'de>, { let result = Status::deserialize(deserializer); match result { Ok(status) => Ok(Some(status)), Err(_) => Ok(None), // Tolerate unknown values } } ```
Prevention
- Always include a catch-all
#[serde(other)]variant for enums from external APIs - Use
#[serde(alias)]for known alternative spellings or deprecated names - Add integration tests that deserialize sample API responses
- Version your API contracts and test against multiple versions
- Log unknown enum variants for monitoring:
Status::Unknown(v) => warn!("Unknown status: {}", v) - Consider using
serde_reprfor integer-based enums with forward compatibility