How to destructure a struct

Destructure a Rust struct by matching its fields with curly braces in a pattern to extract values into variables.

Unpacking data without the boilerplate

You receive a database row mapped to a UserProfile struct. The struct carries id, username, email, created_at, last_login, bio, and avatar_url. Your current task only needs the email to send a notification. Accessing profile.email works, but as the logic grows, you find yourself typing profile. repeatedly. Worse, you want to rename id to user_id to keep your variable names distinct in a nested scope. Destructuring extracts exactly what you need, binds it to the names you choose, and handles the rest.

Think of a struct as a labeled box. Destructuring is the act of opening the box and pulling out specific items. You can take everything, take only a few, or rename the items as you pull them out. The box itself might disappear in the process, depending on what's inside.

The mechanics of destructuring

Destructuring uses pattern matching syntax to bind struct fields to variables. The pattern mirrors the struct definition but focuses on the fields you care about. You can use destructuring in let bindings, match arms, if let statements, and function arguments.

struct Config {
    host: String,
    port: u16,
    debug: bool,
}

/// Extract fields into local variables.
fn main() {
    let config = Config {
        host: String::from("localhost"),
        port: 8080,
        debug: true,
    };

    // Destructure all fields.
    // Variables are created with the same names as the fields.
    let Config { host, port, debug } = config;

    println!("{host}:{port} debug={debug}");
}

The compiler treats the pattern on the left as a blueprint. It checks that every field in the blueprint exists in the struct. If you list host and port, the compiler verifies the struct has those fields. Field order doesn't matter. Config { port, host, debug } works just as well as the definition order. The compiler matches by name, not position.

Destructuring moves data by default. When you write let Config { host, port, debug } = config;, the values inside config move into host, port, and debug. The variable config becomes unusable. This is the same rule that governs assignment. If Config implements Copy, the compiler generates copies instead of moves. You get new variables, and config stays valid. If Config holds a String or a Vec, it doesn't implement Copy. The move happens. config is gone.

This behavior catches developers coming from languages where assignment always copies. Rust makes the move explicit. If you need config after destructuring, you must borrow it.

/// Borrow the struct to keep the original alive.
fn main() {
    let config = Config {
        host: String::from("localhost"),
        port: 8080,
        debug: true,
    };

    // Borrow the struct with &.
    // Variables become references to the fields.
    let Config { host, port, .. } = &config;

    // host is &String, port is &u16.
    println!("{host}:{port}");

    // config is still usable.
    println!("Original: {}", config.host);
}

Borrowing the whole struct is the preferred convention. You can use ref inside the pattern (let Config { ref host, .. } = config;), but borrowing the value (let Config { host, .. } = &config;) is clearer and less error-prone. The community favors the explicit borrow at the binding site.

Don't fight the borrow checker. If you need the struct later, borrow it.

Ignoring fields and future-proofing

You rarely need every field. Listing fields you don't use creates technical debt. If a library author adds a new field to a struct, code that destructures all fields breaks. The compiler demands you handle every field. Adding .. tells the compiler to ignore the rest.

/// Ignore fields with .. to survive API changes.
fn main() {
    let config = Config {
        host: String::from("localhost"),
        port: 8080,
        debug: true,
    };

    // Extract host and port.
    // .. ignores debug and any future fields.
    let Config { host, port, .. } = config;

    println!("{host}:{port}");
}

Treat .. as a shield against upstream modifications. If you don't use a field, ignore it. This convention prevents breakage when the struct evolves. It also signals to readers that the ignored fields are irrelevant to the current logic.

Nested destructuring and renaming

Structs often contain other structs. Destructuring drills down recursively. You can extract fields from nested structs in a single pattern. You can also rename fields to avoid shadowing or clarify intent.

struct Address {
    city: String,
    zip: String,
}

struct User {
    name: String,
    address: Address,
}

/// Destructure nested structs and rename fields.
fn main() {
    let user = User {
        name: String::from("Alice"),
        address: Address {
            city: String::from("Seattle"),
            zip: String::from("98101"),
        },
    };

    // Extract name and nested city.
    // Rename city to user_city to avoid confusion.
    let User { name, address: Address { city: user_city, .. }, .. } = user;

    println!("{name} lives in {user_city}");
}

The syntax address: Address { city: user_city, .. } binds the address field to a pattern that extracts city into user_city. The inner .. ignores zip. The outer .. ignores any other fields in User. You can rename at any level. User { name: username, .. } binds name to username. The shorthand User { name, .. } binds name to name. Use the explicit form when names clash or when you want to emphasize a different variable name.

Mutability flows through the pattern. let mut Config { host } = config; makes host mutable. You can also put mut on the field name: let Config { mut host } = config;. Both work. If you have multiple fields and only one needs to be mutable, put mut on that field name. let Config { mut host, port } = config; keeps port immutable.

Destructuring in function arguments

Functions can destructure arguments in the signature. This documents requirements and reduces boilerplate. Instead of accepting a struct and immediately destructuring inside the body, destructure in the signature.

/// Destructure in function arguments to document requirements.
fn handle_request(request: Request { method, body }) {
    // method and body are available directly.
    // No need to write request.method.
    println!("Handling {method} with body length {}", body.len());
}

struct Request {
    method: String,
    path: String,
    headers: Vec<String>,
    body: Vec<u8>,
}

This approach shows callers that only method and body matter. It reduces indentation and boilerplate in the function body. The compiler still enforces that the argument matches the pattern. Callers must provide a Request with those fields. This is a strong signal of intent. Use it when the function depends on specific fields and doesn't need the whole struct.

Pitfalls and compiler errors

Destructuring moves data. Trying to use a moved struct triggers E0382 (use of moved value). The compiler points to the destructuring line as the move site.

struct Point { x: i32, y: i32 }

fn main() {
    let p = Point { x: 10, y: 20 };

    // p is moved here.
    let Point { x, y } = p;

    // Error: E0382 use of moved value `p`.
    // println!("{}", p.x);
}

Forgetting a field triggers E0027 (missing field in pattern). The compiler lists the missing fields.

struct Point { x: i32, y: i32 }

fn main() {
    let p = Point { x: 10, y: 20 };

    // Error: E0027 missing field `y` in pattern.
    // let Point { x } = p;
}

Adding .. fixes this. let Point { x, .. } = p; compiles. The compiler also warns if you destructure a field and never use it. Use _ to suppress the warning. let Point { x, y: _ } = p; or let Point { x, .. } = p; works. The _ pattern discards the value without binding it.

Renaming syntax is field: new_name. Writing Point { x: px } binds x to px. Writing Point { x } binds x to x. The shorthand is common, but the explicit form is necessary when names clash. If you shadow a variable, the compiler allows it, but be careful. let x = 5; let Point { x } = p; creates a new x that shadows the old one. This is valid Rust, but it can confuse readers. Use renaming to avoid shadowing when clarity matters.

Destructuring is a move. If the struct isn't Copy, the original is gone.

Decision: destructuring vs field access

Use destructuring when you need multiple fields and want clean variable names. Use .. when you only need a subset and want future-proofing against added fields. Use references (&Struct { .. }) when you need to keep the original struct alive after extraction. Reach for .field access when you only need one field or need to access fields conditionally based on logic. Destructuring is a declaration of intent. It says exactly which fields your code depends on at that point.

Where to go next