How does serde::de::Unexpected type help provide meaningful error messages during custom deserialization?

serde::de::Unexpected is an enum that represents what type of value was encountered during deserialization that didn't match expectations, enabling deserializers to report exactly what went wrong by pairing the unexpected value with what was expected. When combined with Error::invalid_type, Error::invalid_value, and related methods, it produces error messages like "invalid type: expected u32, got string" instead of generic deserialization failures.

The Purpose of Unexpected

use serde::de::{self, Unexpected};
 
// Unexpected represents what was found in the input
fn unexpected_variants() {
    // What if we expected a number but got a string?
    let unexpected_str = Unexpected::Str("hello");
    // Error: "invalid type: expected `i32`, got string \"hello\""
    
    // What if we expected a boolean but got a number?
    let unexpected_num = Unexpected::Signed(42);
    // Error: "invalid type: expected `bool`, got integer `42`"
    
    // What if we expected a string but got a boolean?
    let unexpected_bool = Unexpected::Bool(true);
    // Error: "invalid type: expected `String`, got boolean `true`"
    
    // The Unexpected value tells users EXACTLY what was in the input
}

Unexpected variants carry the actual value that was encountered, making errors precise.

All Unexpected Variants

use serde::de::Unexpected;
 
fn all_unexpected_variants() {
    // Primitive types found in input
    let _bool = Unexpected::Bool(true);           // boolean value
    let _signed = Unexpected::Signed(-42);         // signed integer
    let _unsigned = Unexpected::Unsigned(42u64);   // unsigned integer
    let _float = Unexpected::Float(3.14);         // floating point
    let _char = Unexpected::Char('a');             // character
    let _str = Unexpected::Str("hello");           // string slice
    
    // Complex types found in input
    let _bytes = Unexpected::Bytes(&[1, 2, 3]);    // byte slice
    let _none = Unexpected::None;                  // null/None
    let _unit = Unexpected::Unit;                  // unit value ()
    let _seq = Unexpected::Seq;                    // sequence/array
    let _map = Unexpected::Map;                    // map/object
    
    // For unknown types
    let _other = Unexpected::Other("description"); // any other type
}

Each variant captures what type was found in the deserialization input.

Basic Error Reporting

use serde::de::{self, Error, Unexpected};
 
// Manual error creation with Unexpected
fn deserialize_example() -> Result<i32, de::value::Error> {
    // Suppose we received "hello" but expected an integer
    let unexpected = Unexpected::Str("hello");
    
    // Create an error with what we got vs what we expected
    Err(de::value::Error::invalid_type(unexpected, &"i32"))
    
    // This produces: "invalid type: expected `i32`, got string \"hello\""
}
 
// The signature of invalid_type:
// fn invalid_type(unexpected: Unexpected, expected: &dyn Expected) -> Error
 
// Expected is implemented for types that can describe themselves
// - &str implements Expected (describes itself as the string)
// - type name implements Expected (describes itself as the type name)

invalid_type pairs what was found with what was expected.

Implementing Expected Trait

use serde::de::{self, Expected, Unexpected};
use std::fmt;
 
// You can implement Expected for custom type descriptions
struct RangeExpected {
    min: i32,
    max: i32,
}
 
impl Expected for RangeExpected {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        write!(formatter, "an integer between {} and {}", self.min, self.max)
    }
}
 
fn validate_range() -> Result<i32, de::value::Error> {
    let value: i32 = 150;  // Suppose input was 150
    let min = 0;
    let max = 100;
    
    if value < min || value > max {
        // Report the unexpected value with custom expected description
        return Err(de::value::Error::invalid_value(
            Unexpected::Signed(value as i64),
            &RangeExpected { min, max },
        ));
    }
    
    Ok(value)
    // Error: "invalid value: expected an integer between 0 and 100, got integer `150`"
}

Custom Expected implementations provide domain-specific error messages.

Custom Deserializer Example

use serde::de::{self, Deserialize, Deserializer, Error, Unexpected, Visitor};
use std::fmt;
use std::marker::PhantomData;
 
// A type that only accepts positive integers
struct PositiveInt<T>(T);
 
impl<'de, T> Deserialize<'de> for PositiveInt<T>
where
    T: TryFrom<i64> + fmt::Display,
{
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        // Visit a generic integer
        struct PositiveIntVisitor<T>(PhantomData<T>);
        
        impl<'de, T> Visitor<'de> for PositiveIntVisitor<T>
        where
            T: TryFrom<i64> + fmt::Display,
        {
            type Value = PositiveInt<T>;
            
            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a positive integer")
            }
            
            // Handle signed integers
            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
            where
                E: Error,
            {
                if v <= 0 {
                    // Use Unexpected with the actual value
                    return Err(Error::invalid_value(
                        Unexpected::Signed(v),
                        &"a positive integer",
                    ));
                }
                T::try_from(v)
                    .map(PositiveInt)
                    .map_err(|_| Error::custom("integer overflow"))
            }
            
            // Handle unsigned integers
            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
            where
                E: Error,
            {
                if v == 0 {
                    return Err(Error::invalid_value(
                        Unexpected::Unsigned(v),
                        &"a positive integer",
                    ));
                }
                T::try_from(v as i64)
                    .map(PositiveInt)
                    .map_err(|_| Error::custom("integer overflow"))
            }
            
            // Handle non-integer types
            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: Error,
            {
                // Try to parse, or report unexpected string
                match v.parse::<i64>() {
                    Ok(n) if n > 0 => {
                        T::try_from(n)
                            .map(PositiveInt)
                            .map_err(|_| Error::custom("integer overflow"))
                    }
                    Ok(n) => Err(Error::invalid_value(
                        Unexpected::Other(&format!("parsed integer {}", n)),
                        &"a positive integer",
                    )),
                    Err(_) => Err(Error::invalid_type(
                        Unexpected::Str(v),
                        &"a positive integer",
                    )),
                }
            }
        }
        
        deserializer.deserialize_i64(PositiveIntVisitor(PhantomData))
    }
}
 
fn example_usage() {
    let result: Result<PositiveInt<i32>, _> = serde_json::from_str("-5");
    // Error: "invalid value: expected a positive integer, got integer `-5`"
    
    let result2: Result<PositiveInt<i32>, _> = serde_json::from_str("\"hello\"");
    // Error: "invalid type: expected a positive integer, got string \"hello\""
}

The visit_* methods use Unexpected to report what was actually received.

Error Methods Comparison

use serde::de::{self, Error, Unexpected};
 
fn error_methods_comparison() -> Result<(), de::value::Error> {
    // invalid_type: wrong fundamental type (expected number, got string)
    Err(Error::invalid_type(Unexpected::Str("hello"), &"an integer"))?;
    // "invalid type: expected an integer, got string \"hello\""
    
    // invalid_value: correct type but wrong value (negative, but expected positive)
    Err(Error::invalid_value(Unexpected::Signed(-5), &"a positive integer"))?;
    // "invalid value: expected a positive integer, got integer `-5`"
    
    // invalid_length: sequence has wrong number of elements
    Err(Error::invalid_length(2, &"exactly 3 elements"))?;
    // "invalid length 2, expected exactly 3 elements"
    
    // unknown_variant: enum variant doesn't exist
    Err(Error::unknown_variant("red", &["blue", "green"]))?;
    // "unknown variant `red`, expected one of `blue`, `green`"
    
    // unknown_field: struct field doesn't exist
    Err(Error::unknown_field("name", &["id", "value"]))?;
    // "unknown field `name`, expected one of `id`, `value`"
    
    // missing_field: required field is absent
    Err(Error::missing_field("id"))?;
    // "missing field `id`"
    
    // duplicate_field: field appears twice
    Err(Error::duplicate_field("id"))?;
    // "duplicate field `id`"
    
    Ok(())
}

Different Error methods handle different failure modes, each using Unexpected appropriately.

Deserializing Enums with Good Errors

use serde::de::{self, Deserialize, Deserializer, Error, Unexpected, VariantAccess, Visitor};
use std::fmt;
 
#[derive(Debug)]
enum Color {
    Red,
    Green,
    Blue,
    Rgb(u8, u8, u8),
}
 
impl<'de> Deserialize<'de> for Color {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct ColorVisitor;
        
        impl<'de> Visitor<'de> for ColorVisitor {
            type Value = Color;
            
            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a color name (\"red\", \"green\", \"blue\") or RGB object")
            }
            
            fn visit_str<E>(self, v: &str) -> Result<Color, E>
            where
                E: Error,
            {
                match v {
                    "red" => Ok(Color::Red),
                    "green" => Ok(Color::Green),
                    "blue" => Ok(Color::Blue),
                    other => {
                        // Report exactly what string we got
                        Err(Error::unknown_variant(
                            other,
                            &["red", "green", "blue"],
                        ))
                        // Error: "unknown variant `purple`, expected one of `red`, `green`, `blue`"
                    }
                }
            }
            
            fn visit_map<M>(self, mut map: M) -> Result<Color, M::Error>
            where
                M: de::MapAccess<'de>,
            {
                let variant = map.next_key::<&str>()?;
                
                match variant {
                    Some("Rgb") => {
                        let rgb = map.next_value::<(u8, u8, u8)>()?;
                        Ok(Color::Rgb(rgb.0, rgb.1, rgb.2))
                    }
                    Some(other) => {
                        // Report the unknown variant name
                        Err(Error::unknown_variant(other, &["Rgb"]))
                    }
                    None => {
                        // Report unexpected end of input
                        Err(Error::missing_field("variant"))
                    }
                }
            }
        }
        
        deserializer.deserialize_any(ColorVisitor)
    }
}

Enum deserialization uses unknown_variant to report unrecognized values.

Deserializing Structs with Field Validation

use serde::de::{self, Deserialize, Deserializer, Error, MapAccess, Unexpected, Visitor};
use std::fmt;
 
struct User {
    id: u64,
    name: String,
    age: u8,
}
 
impl<'de> Deserialize<'de> for User {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct UserVisitor;
        
        impl<'de> Visitor<'de> for UserVisitor {
            type Value = User;
            
            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a User struct with id (positive u64), name (string), and age (0-150)")
            }
            
            fn visit_map<M>(self, mut map: M) -> Result<User, M::Error>
            where
                M: MapAccess<'de>,
            {
                let mut id = None;
                let mut name = None;
                let mut age = None;
                
                while let Some(key) = map.next_key::<&str>()? {
                    match key {
                        "id" => {
                            if id.is_some() {
                                return Err(Error::duplicate_field("id"));
                            }
                            let value: i64 = map.next_value()?;
                            if value <= 0 {
                                // Report invalid value
                                return Err(Error::invalid_value(
                                    Unexpected::Signed(value),
                                    &"a positive integer",
                                ));
                            }
                            id = Some(value as u64);
                        }
                        "name" => {
                            if name.is_some() {
                                return Err(Error::duplicate_field("name"));
                            }
                            name = Some(map.next_value()?);
                        }
                        "age" => {
                            if age.is_some() {
                                return Err(Error::duplicate_field("age"));
                            }
                            let value: i64 = map.next_value()?;
                            if value < 0 || value > 150 {
                                // Custom expected message
                                struct AgeExpected;
                                impl Expected for AgeExpected {
                                    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
                                        write!(f, "an age between 0 and 150")
                                    }
                                }
                                return Err(Error::invalid_value(
                                    Unexpected::Signed(value),
                                    &AgeExpected,
                                ));
                            }
                            age = Some(value as u8);
                        }
                        other => {
                            // Report unknown field
                            return Err(Error::unknown_field(other, &["id", "name", "age"]));
                        }
                    }
                }
                
                // Check required fields
                let id = id.ok_or_else(|| Error::missing_field("id"))?;
                let name = name.ok_or_else(|| Error::missing_field("name"))?;
                let age = age.ok_or_else(|| Error::missing_field("age"))?;
                
                Ok(User { id, name, age })
            }
        }
        
        deserializer.deserialize_struct("User", &["id", "name", "age"], UserVisitor)
    }
}
 
use serde::de::Expected;
 
fn example_errors() {
    // Invalid age
    let result: Result<User, _> = serde_json::from_str(
        r#"{"id": 1, "name": "Alice", "age": 200}"#
    );
    // Error: "invalid value: expected an age between 0 and 150, got integer `200`"
    
    // Negative id
    let result2: Result<User, _> = serde_json::from_str(
        r#"{"id": -5, "name": "Bob", "age": 30}"#
    );
    // Error: "invalid value: expected a positive integer, got integer `-5`"
    
    // Unknown field
    let result3: Result<User, _> = serde_json::from_str(
        r#"{"id": 1, "name": "Carol", "age": 25, "email": "carol@example.com"}"#
    );
    // Error: "unknown field `email`, expected one of `id`, `name`, `age`"
}

Structured error reporting helps users fix their input.

Combining Unexpected with Custom Errors

use serde::de::{self, Deserialize, Deserializer, Error, Unexpected, Visitor};
use std::fmt;
 
// A port number that must be in valid range
struct Port(u16);
 
impl<'de> Deserialize<'de> for Port {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct PortVisitor;
        
        impl<'de> Visitor<'de> for PortVisitor {
            type Value = Port;
            
            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a port number (1-65535)")
            }
            
            fn visit_u64<E>(self, v: u64) -> Result<Port, E>
            where
                E: Error,
            {
                if v == 0 {
                    // Port 0 is reserved
                    Err(Error::invalid_value(
                        Unexpected::Unsigned(v),
                        &"a non-zero port number",
                    ))
                } else if v > 65535 {
                    // Port too large
                    struct MaxPortExpected;
                    impl Expected for MaxPortExpected {
                        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
                            write!(f, "a port number at most 65535")
                        }
                    }
                    Err(Error::invalid_value(
                        Unexpected::Unsigned(v),
                        &MaxPortExpected,
                    ))
                } else {
                    Ok(Port(v as u16))
                }
            }
            
            fn visit_i64<E>(self, v: i64) -> Result<Port, E>
            where
                E: Error,
            {
                if v < 0 {
                    Err(Error::invalid_value(
                        Unexpected::Signed(v),
                        &"a positive port number",
                    ))
                } else {
                    self.visit_u64(v as u64)
                }
            }
            
            fn visit_str<E>(self, v: &str) -> Result<Port, E>
            where
                E: Error,
            {
                // Try to parse as number
                match v.parse::<u64>() {
                    Ok(n) => self.visit_u64(n),
                    Err(_) => Err(Error::invalid_type(
                        Unexpected::Str(v),
                        &"a port number",
                    )),
                }
            }
        }
        
        deserializer.deserialize_u64(PortVisitor)
    }
}
 
fn port_errors() {
    // Port 0
    let result: Result<Port, _> = serde_json::from_str("0");
    // Error: "invalid value: expected a non-zero port number, got integer `0`"
    
    // Port too large
    let result: Result<Port, _> = serde_json::from_str("70000");
    // Error: "invalid value: expected a port number at most 65535, got integer `70000`"
    
    // Negative port
    let result: Result<Port, _> = serde_json::from_str("-1");
    // Error: "invalid value: expected a positive port number, got integer `-1`"
    
    // String port
    let result: Result<Port, _> = serde_json::from_str("\"http\"");
    // Error: "invalid type: expected a port number, got string \"http\""
}

Domain-specific validation uses Unexpected to report exactly what was wrong.

Unexpected for Sequence Length Errors

use serde::de::{self, Deserialize, Deserializer, Error, SeqAccess, Unexpected, Visitor};
use std::fmt;
 
// A fixed-size array that must have exactly 3 elements
struct Point3D(f64, f64, f64);
 
impl<'de> Deserialize<'de> for Point3D {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct Point3DVisitor;
        
        impl<'de> Visitor<'de> for Point3DVisitor {
            type Value = Point3D;
            
            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a 3-element array of floats [x, y, z]")
            }
            
            fn visit_seq<A>(self, mut seq: A) -> Result<Point3D, A::Error>
            where
                A: SeqAccess<'de>,
            {
                let x = seq.next_element()?
                    .ok_or_else(|| Error::invalid_length(0, &self))?;
                let y = seq.next_element()?
                    .ok_or_else(|| Error::invalid_length(1, &self))?;
                let z = seq.next_element()?
                    .ok_or_else(|| Error::invalid_length(2, &self))?;
                
                // Check for extra elements
                if seq.next_element::<f64>()?.is_some() {
                    return Err(Error::invalid_length(4, &"exactly 3 elements"));
                }
                
                Ok(Point3D(x, y, z))
            }
        }
        
        deserializer.deserialize_seq(Point3DVisitor)
    }
}
 
fn length_errors() {
    // Too few elements
    let result: Result<Point3D, _> = serde_json::from_str("[1.0, 2.0]");
    // Error: "invalid length 2, expected a 3-element array of floats [x, y, z]"
    
    // Too many elements
    let result: Result<Point3D, _> = serde_json::from_str("[1.0, 2.0, 3.0, 4.0]");
    // Error: "invalid length 4, expected exactly 3 elements"
}

invalid_length reports sequence length mismatches without needing Unexpected.

Using Unexpected::Other for Custom Cases

use serde::de::{self, Deserialize, Deserializer, Error, Unexpected, Visitor};
use std::fmt;
 
// A type that must match a specific regex pattern
struct Username(String);
 
impl<'de> Deserialize<'de> for Username {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct UsernameVisitor;
        
        impl<'de> Visitor<'de> for UsernameVisitor {
            type Value = Username;
            
            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a username (alphanumeric, 3-20 characters)")
            }
            
            fn visit_str<E>(self, v: &str) -> Result<Username, E>
            where
                E: Error,
            {
                // Validate length
                if v.len() < 3 {
                    return Err(Error::invalid_value(
                        Unexpected::Other(&format!("string of length {}", v.len())),
                        &"a username of at least 3 characters",
                    ));
                }
                
                if v.len() > 20 {
                    return Err(Error::invalid_value(
                        Unexpected::Other(&format!("string of length {}", v.len())),
                        &"a username of at most 20 characters",
                    ));
                }
                
                // Validate characters
                if !v.chars().all(|c| c.is_alphanumeric()) {
                    // Find the invalid character
                    let invalid = v.chars()
                        .find(|c| !c.is_alphanumeric())
                        .unwrap();
                    return Err(Error::invalid_value(
                        Unexpected::Other(&format!("string with character '{}'", invalid)),
                        &"a username with only alphanumeric characters",
                    ));
                }
                
                Ok(Username(v.to_string()))
            }
        }
        
        deserializer.deserialize_str(UsernameVisitor)
    }
}
 
fn username_errors() {
    // Too short
    let result: Result<Username, _> = serde_json::from_str("\"ab\"");
    // Error: "invalid value: expected a username of at least 3 characters, got string of length 2"
    
    // Invalid character
    let result: Result<Username, _> = serde_json::from_str("\"user@name\"");
    // Error: "invalid value: expected a username with only alphanumeric characters, got string with character '@'"
}

Unexpected::Other handles cases that don't fit other variants.

Summary Table

fn summary() {
    // | Error Method          | Use Case                            | Example Message                    |
    // |-----------------------|-------------------------------------|-------------------------------------|
    // | invalid_type          | Wrong fundamental type              | "expected `i32`, got string"        |
    // | invalid_value         | Correct type, invalid value         | "expected positive, got `-5`"       |
    // | invalid_length        | Wrong sequence length               | "expected 3 elements, got 2"         |
    // | unknown_variant       | Unknown enum variant                | "unknown variant `red`, expected..." |
    // | unknown_field         | Unknown struct field                | "unknown field `foo`, expected..."   |
    // | missing_field         | Required field absent               | "missing field `id`"                |
    // | duplicate_field       | Field appears twice                 | "duplicate field `id`"              |
    
    // | Unexpected Variant    | When to Use                         |
    // |-----------------------|-------------------------------------|
    // | Bool                  | Found boolean where unexpected      |
    // | Signed                | Found negative or signed integer    |
    // | Unsigned              | Found positive unsigned integer     |
    // | Float                 | Found floating-point number          |
    // | Char                  | Found character                     |
    // | Str                   | Found string                        |
    // | Bytes                 | Found byte sequence                 |
    // | None                  | Found null/None                     |
    // | Unit                  | Found unit value ()                 |
    // | Seq                   | Found sequence/array                |
    // | Map                   | Found map/object                    |
    // | Other                 | Custom/unusual cases                |
}

Synthesis

Quick reference:

use serde::de::{self, Error, Unexpected};
 
// Report type mismatch
let err = Error::invalid_type(
    Unexpected::Str("hello"),
    &"an integer"
);
 
// Report value mismatch (correct type, wrong value)
let err = Error::invalid_value(
    Unexpected::Signed(-5),
    &"a positive integer"
);
 
// Report sequence length mismatch
let err = Error::invalid_length(2, &"exactly 3 elements");
 
// Report unknown enum variant
let err = Error::unknown_variant("red", &["blue", "green"]);
 
// Report unknown struct field
let err = Error::unknown_field("extra", &["id", "name"]);

Key insight: serde::de::Unexpected bridges the gap between what was actually in the deserialization input and what your deserializer expected, enabling error messages that precisely identify the mismatch. Instead of generic errors like "deserialization failed," users see "invalid type: expected i32, got string "hello"" or "invalid value: expected a positive integer, got integer -5". The Unexpected enum's variants correspond directly to the visit_* methods in the Visitor trait—when your visit_i64 receives a negative number but you need positive values, you construct Unexpected::Signed(-5) to report exactly what was received. The Error::invalid_type method is for fundamental type mismatches (expected a number, got a string), while Error::invalid_value is for value-range mismatches (expected a positive number, got a negative one). Both methods pair Unexpected with something implementing the Expected trait, which can be as simple as &"description" for one-off messages or a custom type implementing Expected for reusable error descriptions. This pairing produces the familiar serde error format that makes debugging deserialization issues tractable.