How to Use the syn and quote Crates for Proc Macros

Use syn to parse Rust code into an AST and quote to generate new code from that structure for procedural macros.

When you need code that writes code

You're building a library that helps users serialize structs to JSON. You want them to write #[derive(Serialize)] on their types and get a working implementation for free. You can't manually write the impl block for every struct in the ecosystem. You need a tool that reads the user's struct definition at compile time and generates the correct code based on its fields.

That's what procedural macros do. They let you write Rust code that transforms other Rust code. To do this without reinventing the parser, the ecosystem relies on two crates: syn to read the code into a structured tree, and quote to build new code from that tree.

The parser and the generator

Rust source code arrives at your macro as a stream of tokens. Tokens are the smallest meaningful units: keywords, identifiers, punctuation, literals. A raw token stream is hard to work with. You have to manually check for braces, handle nesting, and track state.

syn solves this. It parses the token stream into an Abstract Syntax Tree (AST). The AST is a hierarchy of structs that represent Rust syntax. A DeriveInput node contains the attributes, generics, identifier, and data of a struct or enum. A Field node contains the visibility, type, and identifier of a struct field. syn handles all the grammar rules so you don't have to.

quote solves the output problem. You write a template that looks like normal Rust code, but you can inject values from the AST using #variable. quote takes your template and the AST nodes, fills in the holes, and produces a new token stream. It handles spacing, comma separation, and hygiene automatically.

Think of syn as a translator who reads a messy handwritten note and converts it into a structured form. quote is the printer that takes that form and produces a clean document. You never touch the raw text directly. You work with the structure.

Treat syn as the source of truth for input structure and quote as the source of truth for output. Never mix string manipulation with AST nodes.

Minimal derive macro

Here is a complete derive macro that adds a hello_macro method to any struct. It uses syn to extract the struct name and quote to generate the implementation.

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Parse the input tokens into a DeriveInput AST node.
    // This validates that the input is a valid struct, enum, or union.
    let ast = syn::parse(input).unwrap();

    // Extract the identifier (the name of the type).
    let name = &ast.ident;

    // Build the output token stream using a quote template.
    // #name injects the identifier from the AST.
    let generated = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };

    // Convert the quote output back to a TokenStream and return it.
    generated.into()
}

Keep the macro function thin. Parse, transform, quote, return. Put complex logic in helper functions that operate on syn types.

How the data flows

The compiler invokes your macro with a TokenStream. This is the currency of procedural macros. It represents the code the user wrote, including the struct definition and any attributes.

You pass the TokenStream to syn::parse. syn attempts to match the tokens against a type you specify. For derive macros, that type is usually DeriveInput. If the tokens match a struct definition, syn returns a DeriveInput struct populated with the parsed data. If the tokens are malformed or don't match, syn returns an error.

Once you have the AST, you extract what you need. ast.ident gives you the type name. ast.data gives you the fields. ast.generics gives you the generic parameters. You can traverse the tree to find specific attributes or filter fields by type.

You then use quote! to construct the output. Inside the quote! macro, you write Rust code as if you were writing it normally. When you need to insert a value from the AST, you use #variable. quote knows how to render every syn type back into tokens. It also supports repetition syntax like #(#fields),* to iterate over lists.

Finally, you convert the result to a TokenStream and return it. The compiler takes your output and inserts it into the user's code as if they had written it themselves.

Use parse_macro_input! instead of syn::parse(input).unwrap() in production macros. The macro captures the source span and generates a proper compiler error at the call site. A raw unwrap() panics inside your macro crate and gives the user a cryptic error pointing to your library code, not their code.

Realistic example with error handling

This macro generates a to_tuple method that converts a struct into a tuple of its fields. It handles errors gracefully and uses repetition syntax to process all fields.

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

#[proc_macro_derive(ToTuple)]
pub fn to_tuple_derive(input: TokenStream) -> TokenStream {
    // Parse with parse_macro_input! for better error reporting.
    let input = parse_macro_input!(input as DeriveInput);

    let name = &input.ident;

    // Ensure we only support structs.
    let fields = match &input.data {
        Data::Struct(data_struct) => &data_struct.fields,
        _ => {
            return syn::Error::new_spanned(&input.ident, "ToTuple only supports structs")
                .to_compile_error()
                .into();
        }
    };

    // Generate the method.
    // #fields iterates over all fields.
    // #(&self.#ident),* repeats the pattern for each field, separated by commas.
    let expanded = quote! {
        impl #name {
            pub fn to_tuple(&self) -> (#(#fields),*) {
                (#(&self.#fields),*)
            }
        }
    };

    expanded.into()
}

The syn::Error::new_spanned call creates an error attached to a specific span in the user's code. to_compile_error() converts it to a token stream that the compiler renders as an error message. This ensures the user sees exactly where the problem is.

Use repetition syntax #(#var),* whenever you have a list. It saves you from writing manual loops and string concatenation. quote handles the separators and the iteration count correctly.

Pitfalls and compiler errors

Procedural macros introduce unique challenges. The compiler errors can be confusing if you don't know what to expect.

If you try to return a proc_macro2::TokenStream from a proc_macro function, you get E0308 (mismatched types). The proc_macro crate and proc_macro2 crate define different types for token streams. Use .into() to convert between them.

If you generate code that references a type without importing it, the user gets E0412 (cannot find type) or E0432 (unresolved import). Your macro is responsible for generating the necessary use statements, or the user must provide them. Don't assume the user's scope contains the types your generated code depends on.

If you use syn::parse and the input is invalid, the error points to your macro crate. The user sees a panic inside your library. Use parse_macro_input! to redirect errors to the user's code.

Hygiene is another concern. quote preserves the hygiene of the tokens you inject. If you inject #name, the generated code uses the identifier from the user's context. This prevents accidental shadowing in many cases. However, proc macros have limited hygiene compared to declarative macros. If you generate a variable name like _temp, it might clash with a variable in the user's scope. Use sufficiently unique names or generate names based on the input type.

Generate imports inside your macro if the generated code depends on them. Don't assume the user's scope has what you need.

Decision matrix

Use syn when you need to parse Rust syntax into a structured AST. Manual token parsing is error-prone and ignores Rust's grammar rules.

Use quote when you need to generate Rust code from an AST. String formatting loses hygiene and breaks on complex tokens.

Use proc_macro2 when you are writing tests for your macro logic. The proc_macro crate only works in proc-macro crates, so you can't unit test with it. proc_macro2 provides compatible types that work in standard crates.

Use parse_macro_input! when you want compiler errors to point to the user's code. Raw syn::parse panics obscure the error location and degrade the user experience.

Reach for syn::parse_quote! when you need to create a small AST node from a literal snippet inside your macro. It's faster than writing a quote! and parsing it separately.

Stick to the syn and quote ecosystem. Reinventing the parser or generator is a trap that leads to bugs and unmaintainable code.

Where to go next