How to Add Default Values to a Struct in Rust

Add default values to Rust structs using the Default trait or derive macro for automatic initialization.

When every field shouldn't be a chore

You are building a configuration struct for a web server. It has a host, a port, a timeout, a max connection count, a log level, and a feature flag list. You want to spin up a test instance where only the port changes. You write the struct literal and start typing out every single field. You copy-paste the defaults from your production config. You change the port. You realize you just wrote twelve lines of code to change one number.

This happens constantly. Structs grow. Initialization bloats. Copy-pasting defaults invites bugs because you miss one field and the struct ends up in a weird state. Rust gives you a way to define the "factory settings" once and reuse them everywhere. The Default trait handles this. It lets you ask for a struct with sensible starting values without listing every field.

What Default actually means

Default is a trait defined in the standard library. It has one method: fn default() -> Self. When a type implements Default, you can call Type::default() to get an instance. The trait represents the "zero" state or the most boring, safe, empty version of a value.

For a String, the default is an empty string. For a Vec, it is an empty vector. For a u32, it is zero. For a bool, it is false. These choices are not random. They represent the absence of data or the neutral element for operations. When you implement Default for your own structs, you are making the same promise: this is the value you get when nothing else is specified.

The Default trait appears everywhere in the standard library. HashMap::default() creates an empty map. Vec::default() creates an empty vector. String::default() creates an empty string. These are often aliases for new(), but Default is special because it is a trait. This matters for generics. If you write a function that takes T: Default, you can call T::default() without knowing what T is. You cannot do that with new() because new() is not a trait method. The trait enables generic initialization.

The derive macro shortcut

Most of the time, you do not write the implementation by hand. You use the #[derive(Default)] attribute. The compiler generates the code for you. It looks at every field in your struct and calls Default::default() on that field's type.

#[derive(Default)]
/// A simple user profile with sensible empty state.
struct UserProfile {
    /// Username defaults to empty string.
    username: String,
    /// Age defaults to 0.
    age: u32,
    /// Tags defaults to empty vector.
    tags: Vec<String>,
}

fn main() {
    // Creates UserProfile { username: "", age: 0, tags: [] }
    let user = UserProfile::default();
}

The compiler expands this derive into a manual implementation that initializes each field with its own default. This works recursively. If a field is a Vec<String>, the compiler calls Vec::default(), which creates an empty vector. Inside that, String defaults are not needed because the vector is empty. The chain holds together as long as every field type implements Default.

When derive fails

The derive macro is strict. If your struct contains a field that does not implement Default, the compilation stops. The compiler emits E0277 (trait bound not satisfied) and points to the offending field.

Imagine your struct has a custom type that requires configuration to create. Or a field that is a function pointer. Or a type from a library that forgot to implement Default. The derive macro cannot guess what you want. It refuses to generate code that would leave a field uninitialized or use a value that does not exist.

You have two paths forward. You can implement Default manually for your struct, or you can add the #[default] attribute to specific fields. The choice depends on how much of the struct needs custom logic.

Manual implementation for full control

When derive cannot do the job, you write the trait implementation yourself. This gives you complete control over every field. You can compute values, read constants, or use helper functions.

/// A server configuration with non-defaultable fields.
struct ServerConfig {
    /// Host must be a valid IP, not empty.
    host: String,
    /// Port must be non-zero.
    port: u16,
    /// Custom logger type without Default.
    logger: Logger,
}

/// A placeholder logger that requires initialization.
struct Logger {
    level: u8,
}

impl Default for ServerConfig {
    fn default() -> Self {
        Self {
            // Provide a safe fallback host.
            host: String::from("127.0.0.1"),
            // Standard HTTP port.
            port: 8080,
            // Initialize logger with default level.
            logger: Logger { level: 3 },
        }
    }
}

The manual implementation is just a function. You return a struct literal. You can use any logic inside. You can call other functions. You can panic if the default state is impossible, though that is rare. The key is that default() must always return a value. It cannot return Option or Result. If you cannot create a default, the type should not implement Default.

Convention aside: The community treats Default as infallible. If creating a default value requires I/O or network access, do not put that in Default. Use a new() method or a builder instead. Default should be cheap and fast.

The struct update syntax

Once you have Default, the real power appears in the struct update syntax. You can create a new instance based on the default and override only the fields you care about. This is the idiomatic way to handle partial initialization.

#[derive(Default)]
struct ServerConfig {
    host: String,
    port: u16,
    timeout: u64,
}

fn main() {
    // Start with defaults, override port.
    let config = ServerConfig {
        port: 9090,
        ..Default::default()
    };
    // host is "127.0.0.1", timeout is 0.
}

The ..Default::default() syntax tells the compiler to fill in the remaining fields from the result of Default::default(). This keeps your code readable. You see exactly what is different from the norm. It also future-proofs your code. If you add a new field to the struct later, existing calls to ..Default::default() continue to compile. The new field gets its default value automatically. You do not need to hunt down every initialization site and add the new field.

Trust the struct update syntax. It prevents initialization drift and keeps your code resilient to struct changes.

Field-level defaults with attributes

Rust now supports the #[default] attribute on struct fields. This bridges the gap between derive and manual implementation. You can derive Default for the whole struct while specifying custom defaults for individual fields.

#[derive(Default)]
struct AppConfig {
    /// Derive handles this: empty string.
    name: String,
    /// Override: default to 42.
    #[default(42)]
    magic_number: u32,
    /// Override: default to a literal.
    #[default = "production"]
    environment: String,
}

The #[default] attribute accepts an expression. You can use literals, constants, or simple function calls. The compiler inlines this value into the generated Default implementation. This is perfect for structs where most fields have standard defaults but a few need specific values. You avoid writing the full manual impl while still getting the behavior you want.

Convention aside: Use #[default] for simple overrides. If the default value requires complex logic, a function call with side effects, or depends on other fields, switch to a manual impl Default. The attribute is for declarative defaults, not procedural logic.

Enums and Default

Enums can also derive Default. The compiler picks the first variant defined in the source code. If the variant has fields, those fields must also implement Default. This is useful for state machines where the initial state is the first variant.

#[derive(Default)]
enum ConnectionState {
    /// First variant becomes the default.
    Disconnected,
    Connecting,
    Connected {
        /// Fields must implement Default.
        ip: String,
    },
}

Be careful with this pattern. Reordering enum variants changes the default. That is a silent behavior change. Document your enum order if you rely on Default. The compiler will not warn you if you move Connected to the top and suddenly your app starts in a connected state with an empty IP.

Pitfalls and compiler errors

The most common error is E0277 when using #[derive(Default)]. The compiler tells you which field lacks the trait. The fix is usually to add #[derive(Default)] to that field's type, or use #[default] on the field, or switch to a manual impl.

Another pitfall is assuming Default means "valid". For a String, empty is valid. For a Vec, empty is valid. For a u32, zero is valid. But for a User struct, an empty username might be invalid for your application logic. Default gives you a syntactically correct value, not necessarily a business-logic correct value. You often need a validation step after creation.

Generic structs can also be tricky. If your struct has a type parameter, the derive macro adds a trait bound.

#[derive(Default)]
struct Wrapper<T> {
    value: T,
}

This generates impl<T: Default> Default for Wrapper<T>. The T must implement Default. If you want Wrapper to be defaultable even when T is not, you cannot use derive. You need a manual impl with a different bound, or you need to wrap T in an Option or a custom default type.

Do not force Default on types that cannot have a sensible default. If a type requires external data to exist, it does not implement Default. Respect the trait contract.

Decision: when to use which approach

Use #[derive(Default)] when every field in your struct already implements Default and the standard defaults work for your use case. This is the fastest path and requires zero maintenance.

Use #[default] attributes when most fields use standard defaults but a few need specific values like constants or literals. This keeps the derive macro active while customizing the output.

Use a manual impl Default when your struct contains fields without Default, or when the default value requires computation, logic, or depends on other fields. This gives you full control over the initialization process.

Use struct update syntax ..Default::default() when you need to create an instance with only a few fields changed from the norm. This is the standard pattern for partial configuration and overrides.

Reach for new() methods instead of Default when the "default" state is not a valid state for your application. If a struct must always have a non-empty name or a positive ID, Default is the wrong tool. Provide a constructor that enforces the invariant.

Where to go next