---
title: How to write derive macro
summary: Create a procedural macro crate with the `proc_macro_derive` attribute to automatically generate code for your types.
lang: en
slug: how-to-write-derive-macro
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-write-derive-macro/
---

# How to write derive macro



## The boilerplate trap

You define a struct `User` with fields `id` and `email`. You need a method `validate` that checks if the email contains an `@`. You write the method. You define `Post` with `title` and `body`. You need `validate` to check the title length. You copy-paste the method, change the field names, and tweak the logic. You define `Comment`. You copy-paste again. Your fingers hurt. You realize you're writing the same pattern over and over, just swapping struct names and field lists.

Rust offers a way out. You write a derive macro. The macro reads the struct definition, inspects the fields, and generates the implementation block automatically. You add `#[derive(Validate)]` to any struct, and the method appears. You write the logic once. The macro handles the repetition.

Stop copy-pasting. Build the machine.

## What a derive macro actually is

A derive macro is a function that runs during compilation. The compiler calls your function when it sees `#[derive(MyMacro)]` on a type. Your function receives the type's definition as a stream of tokens, transforms that stream, and returns new tokens. The compiler inserts the new tokens into the source code as if you had typed them.

Think of a derive macro as a factory stamp. You feed the stamp a blueprint of a struct. The stamp reads the blueprint, checks the fields, and presses out a new implementation block with methods tailored to that struct. You don't write the implementation for every struct. You write the stamp once.

The macro runs at compile time. It writes the code your program runs.

## The skeleton: Cargo.toml and the hook

A derive macro lives in its own crate. The crate must declare itself as a procedural macro. In `Cargo.toml`, set `proc-macro = true` in the `[lib]` section.

```toml
[lib]
proc-macro = true
```

This flag tells Cargo to compile the crate as a procedural macro library. The compiler provides the `proc_macro` crate automatically. You don't list it as a dependency.

Convention aside: name the crate with a `-derive` suffix. If the macro is `Validate`, name the crate `validate-derive`. This signals to users that the crate provides a derive macro. The macro name in code matches the attribute name, not the crate name.

The macro function needs an attribute to bind it to a name. Use `#[proc_macro_derive(Name)]`. The function takes a `TokenStream` and returns a `TokenStream`.

```rust
use proc_macro::TokenStream;

// Bind this function to #[derive(Hello)]
#[proc_macro_derive(Hello)]
pub fn hello(input: TokenStream) -> TokenStream {
    // Return the input unchanged for now
    input
}
```

If you forget `proc-macro = true`, the compiler rejects the crate with `E0463` (can't find crate for `proc_macro`). The `proc_macro` crate only exists for procedural macro crates.

Set `proc-macro = true`. Without it, you're just writing a library that the compiler ignores.

## Anatomy of the input and output

The `TokenStream` is a sequence of tokens. Tokens are the smallest units of Rust syntax: identifiers, keywords, punctuation, literals. Whitespace is preserved but ignored by parsers. The input stream contains the struct definition. The output stream contains the generated code.

Parsing a `TokenStream` manually is tedious. You have to handle nesting, generics, attributes, and error reporting. The ecosystem provides `syn` and `quote` for this work. `syn` parses tokens into a syntax tree. `quote` builds tokens from Rust-like syntax.

Convention aside: use `syn` and `quote` for every derive macro. Reinventing the parser is a rabbit hole with no prize. The community expects macros to use these crates. They handle edge cases, hygiene, and error spans correctly.

Treat `TokenStream` as opaque text. Use `syn` to read it. Use `quote` to write it.

## Realistic example: Generating a method

Here is a complete derive macro that generates a `to_json` method. The method returns a string with the struct name.

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

// Bind to #[derive(ToJson)]
#[proc_macro_derive(ToJson)]
pub fn to_json_derive(input: TokenStream) -> TokenStream {
    // Parse the input tokens into a typed syntax tree
    // parse_macro_input! handles errors and returns a compile error if parsing fails
    let input = parse_macro_input!(input as DeriveInput);

    // Extract the struct or enum name
    let name = &input.ident;

    // Generate the implementation block
    // quote! builds tokens from Rust-like syntax
    // #name interpolates the identifier into the output
    let expanded = quote! {
        impl #name {
            pub fn to_json(&self) -> String {
                format!("{{ \"type\": \"{}\" }}", stringify!(#name))
            }
        }
    };

    // Convert the generated tokens back to a TokenStream
    expanded.into()
}
```

The `parse_macro_input!` macro calls `syn::parse`. If the input isn't a valid struct or enum, `syn` generates a compile error pointing to the problematic token. The `quote!` macro builds the output. `#name` inserts the identifier. `stringify!(#name)` converts the identifier to a string literal at compile time.

Trust `syn` and `quote`. Reinventing the parser is a rabbit hole with no prize.

## Parsing fields and generating logic

Derive macros shine when they inspect fields. `syn::DeriveInput` contains a `data` field. For structs, `data` is `syn::Data::Struct`, which holds `syn::Fields`. You can iterate over fields to generate code for each one.

This example generates a method that prints every field name and value.

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

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

    // Match on the data variant to get struct fields
    let fields = match &input.data {
        Data::Struct(data_struct) => &data_struct.fields,
        _ => panic!("DebugPrint only supports structs"),
    };

    // Generate a match arm or print statement for each field
    // fields.iter() yields Field objects
    let field_prints = fields.iter().map(|field| {
        let field_name = &field.ident;
        // Use format_args! to build the print call
        quote! {
            println!("{}: {:?}", #field_name, self.#field_name);
        }
    });

    let expanded = quote! {
        impl #name {
            pub fn debug_print(&self) {
                #(#field_prints)*
            }
        }
    };

    expanded.into()
}
```

The `#(#field_prints)*` syntax repeats the block for each item in the iterator. This is how you generate code dynamically based on the number of fields.

Inspect the struct to generate precise code. The macro knows exactly what it's looking at.

## Error handling: Pointing fingers correctly

Macros can fail. If the user applies `#[derive(ToJson)]` to an enum, and you only support structs, you should report an error. Never use `panic!` in a macro. Panics produce unhelpful compiler messages. Use `syn::Error` to generate a compile error with a span.

```rust
use syn::{Error, Span};

fn check_struct(data: &syn::Data) -> Result<(), Error> {
    match data {
        syn::Data::Struct(_) => Ok(()),
        _ => {
            // Create an error at the span of the derive attribute
            // The span points to #[derive(MyMacro)] in user code
            Err(Error::new_spanned(data, "MyMacro only supports structs"))
        }
    }
}
```

Return the error using `to_compile_error()`. This converts the `syn::Error` into a `TokenStream` that the compiler displays as an error.

```rust
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;

// Check constraints
if let Err(e) = check_struct(&input.data) {
    return e.to_compile_error().into();
}
```

Convention aside: always use `Error::new_spanned` or `Error::new` with a specific span. Point the error to the user's code, not your macro code. The user needs to know where the problem is.

Never panic in a macro. Return a compile error that points to the user's code.

## Pitfalls and compiler errors

Derive macros introduce unique failure modes.

If you forget `proc-macro = true`, you get `E0463` (can't find crate for `proc_macro`). The compiler cannot find the procedural macro API.

If your macro returns the wrong type, you get `E0308` (mismatched types). The function must return `TokenStream`.

Macros are hygienic. Identifiers generated by `quote` do not capture variables from the caller's scope. This prevents name collisions. If you need to reference a variable from the caller, you must pass it explicitly or use a different macro type.

If `syn` fails to parse, `parse_macro_input!` generates an error. If you catch the error and ignore it, the macro might generate invalid code. Always handle parse errors.

Macros run at compile time. Expensive operations slow down compilation. Keep macro logic fast. Avoid heavy computation or I/O.

Macros are code generators. If the generated code is wrong, the compiler blames the macro. Test the output.

## Decision: Derive macro vs alternatives

Use a derive macro when you need to generate an `impl` block based on the structure of a type, like adding methods to a struct or enum.

Use a function when the logic operates on a value at runtime and doesn't need to inspect the type's definition at compile time.

Use a trait when you want to define a shared interface that multiple types can implement manually, without code generation.

Use a declarative macro (`macro_rules!`) when you need to generate arbitrary code fragments, not just implementations attached to a type.

Pick the tool that matches the job. Derive macros attach behavior to types. Functions transform data. Traits define contracts.

## Where to go next

- [How to Use the syn and quote Crates for Proc Macros](/en/how-to-use-the-syn-and-quote-crates-for-proc-macros/)
- [When to Use a Macro vs a Function vs a Trait in Rust](/en/when-to-use-a-macro-vs-a-function-vs-a-trait-in-rust/)
- [Error: "no rules expected the token" in Macro — How to Fix](/en/error-no-rules-expected-the-token-in-macro-how-to-fix/)