Error

"invalid type" in Serde — How to Fix

Fix Serde 'invalid type' errors by ensuring your Rust struct field names and types exactly match the incoming JSON data structure.

When the JSON doesn't match the struct

You are building a CLI tool that fetches data from an API. You define a Rust struct, add #[derive(Deserialize)], and call serde_json::from_str. Instead of a populated struct, the program panics with an error like Error("invalid type: string \"Alice\", expected i32", line: 1, column: 15). You look at the JSON. It looks valid. You look at the struct. It looks correct. Rust is refusing to cooperate.

This error is Serde's way of telling you that the shape of your data does not match the shape of your code. Serde is a contract enforcer. Your struct defines the contract. The JSON is the fulfillment. If the JSON contains a string where the struct expects an integer, or an object where the struct expects an array, Serde rejects the data immediately. The "invalid type" error is the breach of contract notification.

Serde is a strict translator

Think of Serde as a customs officer at an international airport. You have a declaration form (your Rust struct) and a suitcase (the JSON payload). The officer checks every item in the suitcase against the form.

If the form says "Laptop" but the suitcase contains a "Watermelon", the officer stops you. The watermelon is a valid object, but it is the wrong type for the laptop slot. Serde does the same. It maps JSON keys to struct fields and JSON values to Rust types. A mismatch in name, type, or structure triggers the error. Serde will not guess. It will not coerce a string into an integer unless you explicitly tell it to. It will not fill in a missing field unless you provide a default.

The error message is precise. It tells you the expected type, the actual type found, and often the location in the JSON. Read the error message carefully. It is the fastest path to the fix.

Minimal example: Type mismatch

The most common cause of this error is a simple type mismatch. JSON numbers can be integers or floats. Rust distinguishes between i32, u64, f32, and f64. JSON strings are always strings. Rust distinguishes between String and &str, and between String and numeric types.

use serde::Deserialize;

/// Configuration for a network service.
#[derive(Debug, Deserialize)]
struct Config {
    /// The port number to listen on.
    port: u16,
    /// The host address.
    host: String,
}

fn main() {
    // JSON has "port" as a string "8080", but Config expects u16.
    let json = r#"{"port": "8080", "host": "localhost"}"#;

    // This returns an Err because "8080" is a string, not a number.
    let result: Result<Config, _> = serde_json::from_str(json);

    match result {
        Ok(config) => println!("{:?}", config),
        Err(e) => eprintln!("Deserialization failed: {}", e),
    }
}

The output shows the mismatch: Deserialization failed: invalid type: string "8080", expected u16 at line 1 column 14. The JSON key port maps to the struct field port. Serde expects a number token. It finds a string token. The deserialization fails.

Fix the JSON to use a number, or change the struct to accept a string and parse it manually. In this case, the JSON should be {"port": 8080, "host": "localhost"}.

Check the quotes. JSON strings are quoted. JSON numbers are not. A common mistake is sending "8080" instead of 8080 from a frontend or another service.

Realistic scenario: API response

Real-world APIs rarely match Rust naming conventions perfectly. APIs often use camelCase for keys, while Rust uses snake_case. APIs might omit optional fields. APIs might use nested objects that need flattening. Serde provides attributes to bridge these gaps without writing custom deserialization logic.

use serde::Deserialize;

/// Response from a user management API.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiResponse {
    /// HTTP status code.
    status: u16,
    /// List of users.
    users: Vec<User>,
}

/// A user record from the API.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct User {
    /// Unique identifier.
    id: u64,
    /// Display name.
    name: String,
    /// Email address. JSON key is "emailAddress".
    #[serde(rename = "emailAddress")]
    email: String,
    /// Bio text. May be missing in JSON.
    #[serde(default)]
    bio: String,
    /// Admin flag. May be missing.
    #[serde(default)]
    is_admin: bool,
}

fn main() {
    let json = r#"{
        "status": 200,
        "users": [
            {
                "id": 1,
                "name": "Alice",
                "emailAddress": "alice@example.com",
                "isAdmin": true
            },
            {
                "id": 2,
                "name": "Bob",
                "emailAddress": "bob@example.com"
            }
        ]
    }"#;

    let response: ApiResponse = serde_json::from_str(json).expect("Valid JSON structure");
    println!("{:#?}", response);
}

This code handles three common mismatches. The rename_all = "camelCase" attribute tells Serde to map status to status, users to users, and is_admin to isAdmin automatically. The rename = "emailAddress" attribute handles a specific key that doesn't follow the general pattern. The default attribute tells Serde to use the type's default value if the key is missing. For String, the default is an empty string. For bool, the default is false.

The community convention is to use rename_all at the struct level whenever the naming convention differs. It saves you from annotating every field. Use rename only for outliers. Use default for fields that are truly optional in the data model but required in the struct type.

Serde won't save you from a typo. If you misspell a key in rename, you get a compile error. If you misspell a key in the JSON, you get a runtime error. Treat the error message as a precise diagnosis.

The Map vs Sequence trap

A frequent source of confusion is the difference between JSON objects and JSON arrays. In JSON, an object is a map of keys to values, written with curly braces {}. An array is a sequence of values, written with square brackets []. Rust maps JSON objects to structs or HashMaps. Rust maps JSON arrays to Vecs or slices.

If your struct expects a Vec<User> but the JSON contains an object, Serde fails with invalid type: map, expected a sequence. If your struct expects a User but the JSON contains an array, Serde fails with invalid type: sequence, expected struct User.

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct UserList {
    users: Vec<User>,
}

#[derive(Debug, Deserialize)]
struct User {
    name: String,
}

fn main() {
    // JSON has an object where Vec<User> is expected.
    let json = r#"{"users": {"name": "Alice"}}"#;

    let result: Result<UserList, _> = serde_json::from_str(json);
    // Error: invalid type: map, expected a sequence
}

The fix is to align the JSON structure with the Rust type. If the API returns a single object, use a struct. If the API returns a list, use a Vec. Sometimes APIs are inconsistent and return a single object when a list is expected, or vice versa. In those cases, you may need a wrapper struct or a custom deserializer.

Check the brackets. {} means object. [] means array. Mismatched brackets are the cause of map vs sequence errors.

Unknown fields and missing data

By default, Serde ignores unknown fields in JSON. If the JSON contains a key that your struct does not define, Serde skips it and continues. This behavior prevents breaking changes when an API adds new fields. It also hides bugs. If you typo a field name in the JSON, Serde ignores it and the field remains uninitialized or uses its default.

If you want strict validation, add #[serde(deny_unknown_fields)] to the struct. This makes Serde reject any JSON containing keys not defined in the struct.

use serde::Deserialize;

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictConfig {
    port: u16,
}

fn main() {
    let json = r#"{"port": 8080, "host": "localhost"}"#;
    let result: Result<StrictConfig, _> = serde_json::from_str(json);
    // Error: unknown field `host`, expected `port`
}

Missing fields cause a different error: missing field name``. Serde requires all non-optional fields to be present in the JSON. If a field might be missing, use Option<T> or #[serde(default)]. Option<T> makes the field None when missing. #[serde(default)] uses the type's default value.

Use Option<T> when the absence of data is meaningful and you need to distinguish between "missing" and "default". Use #[serde(default)] when a missing field should simply populate with a sensible default.

Treat deny_unknown_fields as a safety net during development. It catches typos in your JSON fixtures. Remove it or keep it based on how strict your API contract is.

Decision matrix

Use #[serde(rename = "json_key")] when the JSON key name differs from your Rust field name due to naming conventions or legacy API constraints.

Use Option<T> for a field when the JSON might omit that key entirely and you want the field to be None instead of causing a deserialization error.

Use #[serde(default)] when a missing field should populate with the type's default value rather than failing or becoming None.

Use #[serde(rename_all = "camelCase")] when the entire struct follows a different naming convention than Rust's snake_case, saving you from annotating every field.

Use #[serde(deny_unknown_fields)] when you want strict validation that rejects JSON containing keys your struct doesn't define, catching typos in the input data.

Use serde_json::Value when the JSON structure is dynamic or unknown at compile time, allowing you to inspect the data programmatically before deserializing into a specific type.

Use #[serde(with = "module")] when you need to transform data during serialization or deserialization, such as parsing a date string into a DateTime struct, and the standard type mismatch cannot be resolved with simple attributes.

Serde gives you the tools to handle almost any mismatch. The key is to read the error, identify the mismatch, and apply the right attribute. Don't fight the compiler. Configure Serde to match your data.

Where to go next