How to rename fields with serde

Rename serde fields using the #[serde(rename = "name")] attribute or #[serde(rename_all = "case")] on the struct.

When Rust names clash with wire names

You define a Rust struct to model a user profile. The field is username. You fetch JSON from an API and try to deserialize it. The JSON contains {"user_name": "alice"}. Serde rejects the data. The error message complains about a missing field named username.

The mismatch is the problem. Serde matches Rust identifiers to wire format keys by default. When the external format uses a different naming convention, or when the API returns a legacy key name, the deserialization fails. Renaming attributes tell serde how to translate between Rust's internal names and the names that appear in the serialized data.

The translation layer

Serde acts as a translator between Rust types and external formats like JSON, TOML, or CSV. At compile time, serde derives code that inspects the structure of your types. It generates logic to read and write values based on field names. By default, the field name in Rust must match the key in the external format exactly.

Renaming attributes modify this mapping. They create a dictionary that serde consults during serialization and deserialization. When you serialize, serde writes the renamed key. When you deserialize, serde looks for the renamed key and maps it to the Rust field. The Rust code continues to use the original field name. The renaming happens only at the boundary where data crosses in or out of your program.

This separation keeps your Rust code idiomatic while allowing flexibility for external contracts. You can use snake_case in Rust and camelCase in JSON without renaming your fields to match the API.

Minimal rename example

The #[serde(rename = "...")] attribute applies to a single field. It sets the wire name for that field while preserving the Rust identifier.

use serde::{Deserialize, Serialize};

/// Represents a user profile fetched from an API.
#[derive(Serialize, Deserialize)]
struct User {
    /// The user's unique identifier.
    #[serde(rename = "user_name")]
    username: String,
}

fn main() {
    // Create a User instance using Rust field names.
    let user = User {
        username: "alice".to_string(),
    };

    // Serialize to JSON. The key becomes "user_name".
    let json = serde_json::to_string(&user).unwrap();
    assert_eq!(json, r#"{"user_name":"alice"}"#);

    // Deserialize from JSON. Serde looks for "user_name".
    let json_input = r#"{"user_name":"bob"}"#;
    let user: User = serde_json::from_str(json_input).unwrap();
    assert_eq!(user.username, "bob");
}

The attribute #[serde(rename = "user_name")] tells serde to use user_name in the JSON output and to search for user_name in the input. The Rust code accesses the field as user.username. The rename affects both serialization and deserialization.

Global renaming with rename_all

Renaming every field individually creates boilerplate. When an entire struct follows a different naming convention, #[serde(rename_all = "...")] applies a case transformation to all fields automatically.

use serde::{Deserialize, Serialize};

/// API response with camelCase keys.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiResponse {
    /// HTTP status code.
    status_code: u32,
    /// Optional error description.
    error_message: Option<String>,
    /// List of items.
    items: Vec<String>,
}

fn main() {
    let response = ApiResponse {
        status_code: 200,
        error_message: None,
        items: vec!["a".to_string(), "b".to_string()],
    };

    // Fields are serialized as statusCode, errorMessage, items.
    let json = serde_json::to_string(&response).unwrap();
    assert_eq!(
        json,
        r#"{"statusCode":200,"items":["a","b"]}"#
    );
}

The rename_all attribute supports a fixed set of case styles: camelCase, snake_case, PascalCase, SCREAMING_SNAKE_CASE, kebab-case, lowercase, UPPERCASE, and titlecase. Serde transforms each field name according to the chosen style. The attribute applies to all fields in the struct unless a field has an explicit rename override.

Explicit renames take precedence over rename_all. If you add #[serde(rename = "data_payload")] to a field, serde uses data_payload regardless of the global case style. This allows you to handle exceptions without disabling the global rule.

Walk through the mechanics

When you derive Serialize or Deserialize, serde expands the derive macro into implementation code. The generated code contains string literals for field names. Renaming attributes modify those literals.

For a struct with #[serde(rename_all = "camelCase")], the generated serialization code writes keys like "statusCode" instead of "status_code". The deserialization code checks for "statusCode" in the input map. If the key is missing, deserialization fails.

The renaming happens at compile time. There is no runtime overhead for the name mapping. Serde bakes the names into the generated code. The only cost is the string comparison during deserialization, which is identical whether you use renaming or not.

Attributes can also appear on enum variants. #[serde(rename = "...")] on a variant changes the tag name in tagged enum representations. #[serde(rename_all = "...")] on an enum changes the tag names for all variants. The behavior mirrors struct fields.

Realistic scenario with mixed conventions

Real APIs often mix conventions. Some fields follow a pattern, while others are legacy names or acronyms. You can combine rename_all with selective rename overrides to handle this cleanly.

use serde::{Deserialize, Serialize};

/// Configuration object from a third-party service.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
struct Config {
    /// Maximum retry attempts.
    max_retries: u32,
    /// Base URL for the service.
    base_url: String,
    /// Legacy field name that doesn't follow kebab-case.
    #[serde(rename = "api_key_v1")]
    api_key: String,
    /// Nested struct keeps its own naming.
    advanced: AdvancedOptions,
}

#[derive(Serialize, Deserialize)]
struct AdvancedOptions {
    timeout_ms: u32,
}

fn main() {
    let config = Config {
        max_retries: 3,
        base_url: "https://example.com".to_string(),
        api_key: "secret".to_string(),
        advanced: AdvancedOptions { timeout_ms: 5000 },
    };

    // Output uses kebab-case for most fields, but "api_key_v1" for the override.
    let json = serde_json::to_string(&config).unwrap();
    println!("{}", json);
}

The struct uses kebab-case globally. Fields like max_retries serialize as max-retries. The api_key field has an explicit rename to api_key_v1, which overrides the global rule. Nested structs like AdvancedOptions do not inherit rename_all from the parent. Each struct manages its own naming independently.

Convention aside: The Rust community prefers rename_all for API wrappers. It reduces repetition and keeps the mapping consistent. Use rename only for fields that break the pattern. If you find yourself adding rename to most fields, reconsider the rename_all choice or accept the wire names in your Rust code.

Pitfalls and debugging

Renaming attributes introduce a layer of indirection. Errors often surface when the mapping is incorrect or incomplete.

If you forget to rename a field that differs from the wire name, deserialization fails with a missing field error. The error message references the Rust field name, not the wire name. This can be confusing if you expect the error to mention the JSON key.

Example error:

Error("missing field `username`", line: 1, column: 2)

The error says username is missing because serde is looking for username in the input. The input contains user_name, which serde ignores. The fix is to add #[serde(rename = "user_name")] to the field.

Another pitfall is precedence confusion. rename always wins over rename_all. If you set rename_all = "camelCase" and add rename = "user_name", the wire name is user_name, not userName. This is intentional, but it can cause bugs if you assume rename_all applies everywhere.

Deserialization aliases can hide mismatches. If you use #[serde(alias = "...")], serde accepts multiple names during deserialization. If you test only with the alias and forget the primary name, your code might work locally but fail in production when the API switches back to the standard name. Always test with the primary wire name.

Convention aside: Keep alias lists short. If you need five aliases for a single field, the API contract is unstable. Consider wrapping the API response in a newtype that handles the translation explicitly, rather than accumulating aliases.

Backward compatibility with alias

The #[serde(alias = "...")] attribute allows a field to deserialize from multiple names. It affects only deserialization. Serialization always uses the primary name or the rename name.

use serde::{Deserialize, Serialize};

/// User data with legacy support.
#[derive(Serialize, Deserialize)]
struct User {
    /// Current wire name is "user_name".
    /// Old API versions used "username".
    #[serde(rename = "user_name", alias = "username")]
    username: String,
}

fn main() {
    // Deserialize from current format.
    let current = r#"{"user_name":"alice"}"#;
    let user: User = serde_json::from_str(current).unwrap();
    assert_eq!(user.username, "alice");

    // Deserialize from legacy format.
    let legacy = r#"{"username":"bob"}"#;
    let user: User = serde_json::from_str(legacy).unwrap();
    assert_eq!(user.username, "bob");

    // Serialization always uses the rename name.
    let json = serde_json::to_string(&user).unwrap();
    assert_eq!(json, r#"{"user_name":"bob"}"#);
}

The alias attribute adds username as an acceptable input key. Serde checks for user_name first. If it's missing, it checks for username. If neither is present, deserialization fails. Serialization ignores the alias and writes user_name.

Use alias to support API versioning. When an API renames a field, you can add the old name as an alias to keep existing clients working. Remove the alias once the old API version is deprecated.

Decision matrix

Use #[serde(rename = "...")] when you need to map a single Rust field to a specific wire name that does not follow a global pattern.

Use #[serde(rename_all = "...")] when your Rust codebase follows one naming convention but the external format uses another, such as snake_case in Rust versus camelCase in JSON.

Use #[serde(alias = "...")] when you need to support multiple names for deserialization, such as maintaining backward compatibility with an older API version or accepting alternative spellings.

Use #[serde(rename_all_fields = "...")] when you want to rename fields inside a nested struct without modifying the nested struct's definition, which is useful for third-party types where you cannot add attributes directly.

Where to go next