#[derive] in Rust: Traits, Serde & Custom Macros

Max WellsMax WellsFounder of Rustify

TL;DR: #[derive(TraitName)] is an attribute that tells the compiler to automatically generate a trait implementation for your struct or enum. It saves you from writing repetitive boilerplate. The compiler generates the implementation by applying the trait to each field recursively. Built-in derivable traits include Debug, Clone, Copy, PartialEq, Eq, Hash, Default, and more. Crates like serde add their own: Serialize, Deserialize.


How Does #[derive] Work?

#[derive] is a procedural macro that runs at compile time, inspecting your type's structure and generating a trait implementation for each field or variant.

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}
 
fn main() {
    let p1 = Point { x: 1.0, y: 2.0 };
    let p2 = p1.clone();             // Clone generated
    println!("{:?}", p1);            // Debug generated
    println!("{}", p1 == p2);        // PartialEq generated; true
}

The generated Debug impl for Point produces Point { x: 1.0, y: 2.0 }. The generated Clone calls .clone() on each field. The generated PartialEq compares each field with ==.


Which Traits Can Be Derived?

The standard library provides 14 derivable traits. Each requires that all fields also implement the same trait.

TraitWhat it generatesRequires
Debug{:?} formattingAll fields: Debug
Clone.clone()All fields: Clone
CopyImplicit copy on assignAll fields: Copy + Clone
PartialEq== and !=All fields: PartialEq
EqFull equality (marker)PartialEq
PartialOrd<, > (partial)All fields: PartialOrd
Ord.cmp(), sortingEq + all fields: Ord
HashUse in HashMap keysAll fields: Hash
DefaultType::default()All fields: Default
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct UserId(u64); // newtype; derives work on tuple structs too
 
#[derive(Debug, Default)]
struct Config {
    timeout_ms: u64,   // default: 0
    retries: u32,      // default: 0
    verbose: bool,     // default: false
}
 
let cfg = Config::default();
println!("{cfg:?}"); // Config { timeout_ms: 0, retries: 0, verbose: false }

What Does serde's #[derive] Add?

serde::Serialize and serde::Deserialize are the most commonly used third-party derive macros; they generate JSON/TOML/YAML serialization code.

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use serde::{Deserialize, Serialize};
 
#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: u64,
    name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    email: Option<String>,
}
 
fn main() {
    let user = User { id: 1, name: "Alice".into(), email: None };
 
    let json = serde_json::to_string(&user).unwrap();
    println!("{json}"); // {"id":1,"name":"Alice"}
 
    let back: User = serde_json::from_str(&json).unwrap();
    println!("{back:?}");
}

When Should You Implement Manually Instead of Deriving?

Derive when the default field-by-field implementation is correct. Implement manually when you need custom logic; ordering by a specific field, redacting secrets from Debug output, or partial equality.

use std::fmt;
 
// Manual Debug; hide the password field
struct Credentials {
    username: String,
    password: String,
}
 
impl fmt::Debug for Credentials {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Credentials")
            .field("username", &self.username)
            .field("password", &"[REDACTED]")
            .finish()
    }
}
 
// Manual PartialEq; compare only by ID, not all fields
#[derive(Debug)]
struct Record {
    id: u64,
    data: Vec<u8>, // large, don't compare
}
 
impl PartialEq for Record {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

How Do You Write a Custom Derive Macro?

Custom derive macros are procedural macros that parse your type's structure at compile time using syn and generate code using quote.

# In a proc-macro crate's Cargo.toml
[lib]
proc-macro = true
 
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"

Writing custom derives is advanced; most Rust developers use existing derives from the ecosystem (serde, thiserror, clap, sqlx) rather than writing their own.


Frequently Asked Questions

Copy is a subtrait of Clone; every Copy type must also be Clone. This makes the type system consistent: if you can implicitly copy a value, you can certainly explicitly clone it.

No; derived implementations require all fields to implement the same trait. If one field doesn't implement Debug, you cannot #[derive(Debug)]. Either implement manually or wrap the field in a type that does implement the trait.

PartialEq allows a != a (like f32::NAN). Eq asserts full reflexive equality (a == a is always true). Eq is a marker trait (no methods) and requires PartialEq. For most types, derive both together.

Procedural macros (like serde's derive) can slow compilation; each derive runs Rust code at compile time. For large codebases, this is usually acceptable. If compile times are critical, consider reducing derive usage or using typetag alternatives.


Sources


  • Trait: #[derive] generates trait implementations automatically
  • Macro: #[derive] is a procedural macro that generates code at compile time
  • Serde: #[derive(Serialize, Deserialize)] is serde's derive API
  • Struct: Derives are most commonly used on structs and enums
  • Enum: #[derive(Debug, PartialEq)] works on enums too
  • Clap: clap's derive API turns structs and enums into CLI parsers
  • Copy/Clone: Copy and Clone are among the most common traits developers derive first
  • proc-macro: Every custom derive in Rust is implemented as a procedural macro under the hood
  • SeaORM: SeaORM relies heavily on derive-based entity definitions and model generation

Keep Reading

Ready to Land a $80-120k Rust Job?