Introduction

Serde's YAML deserialization uses external tagging by default for enums, which requires a specific YAML structure where the variant name is a key and its data is the value. When configuration files use a different structure, deserialization fails with missing field or invalid type errors. This is especially common when migrating from JSON to YAML config files or when hand-writing YAML that does not match Serde's expected format.

Symptoms

  • serde_yaml::Error: missing field 'type' when deserializing enum
  • invalid type: string, expected enum variant for enum fields
  • YAML file parses fine but Rust struct gets wrong enum variant
  • expected a string key but found a sequence for nested enums
  • Deserialization works for some variants but not others

Error output: `` Error: missing field variant_name at line 5 column 3 while parsing a block mapping in "<reader>", line 5, column 3 expected <block end>, but found '<block sequence start>' in "<reader>", line 6, column 5

Common Causes

  • YAML uses internally tagged format but enum uses external tagging
  • Enum variant names in YAML do not match Rust variant names
  • Unit variants serialized as strings vs null
  • Adjacent tagging configuration mismatch between struct and YAML
  • New enum variant added to Rust code but not to existing YAML configs

Step-by-Step Fix

  1. 1.Use externally tagged enum (Serde YAML default):
  2. 2.```rust
  3. 3.use serde::Deserialize;

#[derive(Debug, Deserialize)] enum NotificationConfig { Email { address: String, smtp_host: String }, Slack { webhook_url: String, channel: String }, Webhook { url: String, method: String }, }

// CORRECT YAML format for external tagging: /* notifications: - Email: address: "user@example.com" smtp_host: "smtp.gmail.com" - Slack: webhook_url: "https://hooks.slack.com/services/xxx" channel: "#alerts" */

// WRONG YAML (commonly attempted): /* notifications: - type: Email # This is internal tagging, does not work! address: "user@example.com" smtp_host: "smtp.gmail.com" */

fn load_config() -> Result<Vec<NotificationConfig>, Box<dyn std::error::Error>> { let yaml = std::fs::read_to_string("config.yaml")?; let config: Vec<NotificationConfig> = serde_yaml::from_str(&yaml)?; Ok(config) } ```

  1. 1.Switch to internally tagged enum for simpler YAML:
  2. 2.```rust
  3. 3.use serde::Deserialize;

// Internal tagging allows flatter YAML structure #[derive(Debug, Deserialize)] #[serde(tag = "type")] enum NotificationConfig { Email { address: String, smtp_host: String }, Slack { webhook_url: String, channel: String }, }

// YAML for internally tagged enum (flatter): /* notifications: - type: Email address: "user@example.com" smtp_host: "smtp.gmail.com" - type: Slack webhook_url: "https://hooks.slack.com/services/xxx" channel: "#alerts" */ ```

  1. 1.Use aliasing for YAML-friendly variant names:
  2. 2.```rust
  3. 3.use serde::Deserialize;

#[derive(Debug, Deserialize)] #[serde(rename_all = "snake_case")] enum DatabaseDriver { PostgreSQL, // serialized as "postgreSQL" by default MySql, // serialized as "mySql" #[serde(rename = "sqlite")] // explicit rename SQLite, }

// YAML: /* database: driver: postgre_sql # Or with rename_all: driver: postgre_sql */

// With serde(alias) for backward compatibility: #[derive(Debug, Deserialize)] enum LogLevel { #[serde(alias = "warning")] // Accept both "Warn" and "warning" Warn, #[serde(alias = "error")] Error, Info, Debug, } ```

  1. 1.Handle optional enum variants gracefully:
  2. 2.```rust
  3. 3.use serde::Deserialize;

#[derive(Debug, Deserialize)] struct AppConfig { // Optional enum - None if not specified #[serde(default)] notification: Option<NotificationMethod>,

// Enum with default variant #[serde(default)] log_level: LogLevel, }

#[derive(Debug, Deserialize)] enum NotificationMethod { Email { address: String }, Slack { channel: String }, }

impl Default for LogLevel { fn default() -> Self { LogLevel::Info } }

#[derive(Debug, Deserialize)] enum LogLevel { Trace, Debug, Info, Warn, Error, }

impl Default for LogLevel { fn default() -> Self { LogLevel::Info } } ```

  1. 1.Debug YAML deserialization errors:
  2. 2.```rust
  3. 3.fn debug_yaml_parse(yaml_content: &str) {
  4. 4.// First, parse as generic YAML Value to inspect structure
  5. 5.let value: serde_yaml::Value = match serde_yaml::from_str(yaml_content) {
  6. 6.Ok(v) => v,
  7. 7.Err(e) => {
  8. 8.eprintln!("YAML syntax error: {}", e);
  9. 9.return;
  10. 10.}
  11. 11.};

eprintln!("Parsed YAML structure:"); eprintln!("{}", serde_yaml::to_string(&value).unwrap());

// Then try to deserialize into target type match serde_yaml::from_value::<AppConfig>(value) { Ok(config) => eprintln!("Deserialized successfully: {:?}", config), Err(e) => eprintln!("Deserialization error: {}", e), } } ```

Prevention

  • Use internally tagged enums (#[serde(tag = "type")]) for hand-written YAML configs
  • Add #[serde(rename_all = "snake_case")] for YAML-friendly enum names
  • Write round-trip tests that serialize and deserialize config types
  • Use serde_yaml::Value as an intermediate step for debugging
  • Document the expected YAML structure next to the Rust type definition
  • Validate config files in CI before deployment