Skip to main content

Custom Derive Macro

A custom derive macro is a procedural macro that runs when you write:

#[derive(MyTrait)]
struct MyType { ... }

It:

  • Receives the full definition of the struct/enum.
  • Analyzes its fields, variants, generics, attributes, etc.
  • Generates an implementation of a trait (or other code).

Why Use Custom Derives?​

They are perfect for:

  • Automatically implementing traits based on structure.
  • Reducing boilerplate.
  • Enforcing compile-time rules.
  • Creating declarative APIs.

Examples from the ecosystem:

  • #[derive(Serialize, Deserialize)] → serde
  • #[derive(Parser)] → clap
  • #[derive(Error)] → thiserror

How Custom Derive Macros Work​

  1. The compiler sees #[derive(MyTrait)].
  2. It sends the annotated item as a TokenStream to your macro.
  3. Your macro:
    • Parses the input into a syntax tree (AST).
    • Extracts information (name, fields, generics).
    • Generates Rust code.
  4. The generated code is compiled as if the user wrote it.

Step-by-Step: Build a Custom Derive Macro​

We’ll build #[derive(HelloMacro)] that adds a method to a struct or enum.

Step 1: Create the Proc-Macro Crate​

cargo new hello_macro_derive --lib

In Cargo.toml:

[lib]
proc-macro = true

[dependencies]
syn = "2"
quote = "1"
proc-macro2 = "1"

Step 2: Define the Macro Code​

In lib.rs:

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Parse input tokens into a syntax tree
let input = parse_macro_input!(input as DeriveInput);

let name = input.ident;

// Generate code
let expanded = quote! {
impl HelloMacro for #name {
fn hello() {
println!("Hello from {}!", stringify!(#name));
}
}
};

TokenStream::from(expanded)
}

Step 3: Define the Trait (in a normal crate)​

pub trait HelloMacro {
fn hello();
}

Step 4: Use the Derive Macro​

use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
Pancakes::hello();
}

What Gets Generated​

Conceptually, the compiler expands this into:

impl HelloMacro for Pancakes {
fn hello() {
println!("Hello from Pancakes!");
}
}

Example 2: Derive Macro That Reads Struct Fields​

Let’s create a more realistic macro: #[derive(Describe)] that prints the field names and types.

Macro Implementation

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};

#[proc_macro_derive(Describe)]
pub fn describe_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;

let fields = match input.data {
Data::Struct(data) => match data.fields {
Fields::Named(fields) => fields.named,
_ => panic!("Describe only supports structs with named fields"),
},
_ => panic!("Describe only supports structs"),
};

let field_names = fields.iter().map(|f| {
let name = &f.ident;
quote! {
println!("Field: {}", stringify!(#name));
}
});

let expanded = quote! {
impl Describe for #name {
fn describe() {
println!("Struct {}", stringify!(#name));
#(#field_names)*
}
}
};

TokenStream::from(expanded)
}

Trait Definition

pub trait Describe {
fn describe();
}

Usage

#[derive(Describe)]
struct User {
id: u32,
name: String,
active: bool,
}

fn main() {
User::describe();
}

Expansion (Conceptual)

impl Describe for User {
fn describe() {
println!("Struct User");
println!("Field: id");
println!("Field: name");
println!("Field: active");
}
}

Handling Generics in Derive Macros​

If your struct has generics:

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

You must carry generics into the impl:

let generics = input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

quote! {
impl #impl_generics HelloMacro for #name #ty_generics #where_clause {
fn hello() {
println!("Hello from {}!", stringify!(#name));
}
}
}

Supporting Attributes in Custom Derives​

You can let users customize behavior:

#[derive(Describe)]
#[describe(skip)]
struct User {
id: u32,
password: String,
}

Then parse attributes with syn to change code generation (e.g., skip fields).

Error Handling in Derive Macros​

Instead of panic!, use compile errors:

return syn::Error::new_spanned(
input,
"Describe only supports structs with named fields",
)
.to_compile_error()
.into();

This gives users friendly compiler messages.

When to Use Custom Derives vs Other Macros​

Use CaseBest Tool
Implement trait based on struct shapeCustom derive
Wrap or modify functionsAttribute macro
Custom DSL or syntaxFunction-like macro
Simple repetitionmacro_rules!