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 variant new_feature, expected one of old, 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. 1.Add a catch-all variant with serde other attribute:
  2. 2.```rust
  3. 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. 1.**Use #[serde(alias)] for known alternative names**:
  2. 2.```rust
  3. 3.#[derive(Deserialize, Debug)]
  4. 4.enum Status {
  5. 5.Active,
  6. 6.#[serde(alias = "disabled", alias = "deactivated")]
  7. 7.Inactive,
  8. 8.Pending,
  9. 9.}
  10. 10.`
  11. 11.Handle unknown variants with a custom deserializer:
  12. 12.```rust
  13. 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. 1.Use Option to tolerate missing or unknown values:
  2. 2.```rust
  3. 3.#[derive(Deserialize)]
  4. 4.struct ApiResponse {
  5. 5.#[serde(deserialize_with = "deserialize_status_or_none")]
  6. 6.status: Option<Status>,
  7. 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_repr for integer-based enums with forward compatibility