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!