Fix E0798: Derive of #[serde]-like attribute should produce an item before calling set_proc_macro_attr
1. Symptoms
When compiling a Rust project that uses derive macros (commonly with #[serde(Serialize, Deserialize)] or custom derive macros), you encounter the following error:
error[E0798]: derive of `#[serde]`-like attribute should produce an item before calling `set_proc_macro_attr`
--> src/lib.rs:12:1
|
12 | #[derive(MyCustomDerive)]
| ^^^^^^^^^^^^^^^^^^^^^^
---
The error appears at the `#[derive(...)]` attribute site on a struct, enum, or other item. The compiler message is unambiguous: the derive macro attempted to call `set_proc_macro_attr` (a proc-macro2 function) before producing any output item.
Additional symptoms that may accompany this error:
```text
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0798`.
- The derive macro may fail to expand, leaving the original type untransformed.
- If the macro is used on multiple items, every invocation triggers the same error.
- The error is a hard compile-time failure — no binary or library is produced.
Example code that triggers E0798:
// src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
// This is a simplified version of a broken custom derive macro.
#[proc_macro_derive(MyCustomDerive)]
pub fn my_custom_derive(input: TokenStream) -> TokenStream {
// ❌ Wrong: calling set_proc_macro_attr before producing an item
let input_copy = input.clone();
let attr = proc_macro2::TokenTree::Group(proc_macro2::Group::new(
proc_macro2::Delimiter::Parenthesis,
input_copy,
));
// Simulating what older serde or custom macros might have done:
// proc_macro::set_proc_macro_attr(&attr, || {});
// No TokenStream returned from here causes the issue
// because we haven't produced an output item yet.
TokenStream::new()
}
#[derive(MyCustomDerive)]
struct ExampleStruct {
field: String,
}
2. Root Cause
E0798 is a constraint enforced by the Rust compiler for derive macros. The root cause is a violation of the derive macro contract: the macro calls set_proc_macro_attr (or an equivalent operation) before emitting any output tokens.
What is set_proc_macro_attr?
proc_macro::set_proc_macro_attr is a low-level function in the proc_macro crate that transfers attributes from an input item to an output item. It is commonly used by derive macros that need to propagate attributes like #[serde(...)] to generated code.
The key invariant is:
A derive macro must produce an item in its output token stream before calling
set_proc_macro_attr.
When this invariant is broken, the compiler raises E0798.
Why does this happen?
- Macro author error: A custom derive macro implementation calls
set_proc_macro_attrimmediately, beforequote!-ing any output tokens. - Outdated dependency: A version of
serde_deriveor another procedural macro that predates the enforcement of this rule triggers the error in newer Rust toolchains. - Incorrect proc-macro2 usage: The macro incorrectly assumes it can set attributes on a placeholder item that hasn’t been emitted yet.
The underlying mechanism
The Rust compiler’s proc-macro pipeline works like this:
Input TokenStream → Derive Macro → Output TokenStream → Expand into AST
The set_proc_macro_attr function is designed to work after the macro has produced output tokens. It allows the macro to signal: “Transfer the #[...] attribute from the input to the output item I just generated.” If no output item exists, there is nothing to transfer the attribute to, and the compiler must reject the macro invocation.
3. Step-by-Step Fix
Fix 1: Ensure the derive macro produces output before calling set_proc_macro_attr
The macro must generate an item first, then call set_proc_macro_attr on that generated item.
Before:
// Broken custom derive macro
#[proc_macro_derive(MyCustomDerive)]
pub fn my_custom_derive(input: TokenStream) -> TokenStream {
let input_copy = input.clone();
// ❌ Calling set_proc_macro_attr with no output item produced
proc_macro::set_proc_macro_attr(
&proc_macro2::TokenTree::Group(proc_macro2::Group::new(
proc_macro2::Delimiter::Parenthesis,
input_copy,
)),
|| {},
);
// Returning empty or the input unchanged — no new item produced
TokenStream::new()
}
After:
// Fixed custom derive macro
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(MyCustomDerive)]
pub fn my_custom_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// ✅ First, produce the output item
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = &input.generics.split_for_impl();
let expanded = quote! {
impl #impl_generics MyCustomDerive for #name #ty_generics #where_clause {
fn describe(&self) -> String {
format!("Instance of {}", stringify!(#name))
}
}
};
// ✅ Now set the proc_macro_attr on the output, not before it
// The attribute transfer happens to the generated impl block
let attr_tokens = proc_macro2::TokenTree::Group(proc_macro2::Group::new(
proc_macro2::Delimiter::Parenthesis,
proc_macro2::TokenStream::new(),
));
// Return the properly generated output item
expanded.into()
}
Fix 2: Update serde_derive or serde to a compatible version
If the error originates from #[derive(Serialize, Deserialize)], update your dependencies:
Before (Cargo.toml):
[dependencies]
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.91"
After (Cargo.toml):
[dependencies]
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.134"
Run the update:
cargo update serde
cargo update serde_derive
Verify the updated versions:
cargo tree | grep serde
Expected output:
serde v1.0.217
serde_derive v1.0.217
serde_json v1.0.134
Fix 3: For custom derive macros that must use set_proc_macro_attr
If your use case genuinely requires set_proc_macro_attr, structure the macro so the output item is emitted first:
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;
#[proc_macro_derive(MyAttrDerive, attributes(my_attr))]
pub fn my_attr_derive(input: TokenStream) -> TokenStream {
let derive_input = parse_macro_input!(input as DeriveInput);
let ident = &derive_input.ident;
// Step 1: Generate the output item (e.g., an impl block)
let output = quote! {
impl MyAttrDerive for #ident {
fn trait_method() -> &'static str {
"generated"
}
}
};
// Step 2: Now, if you must use set_proc_macro_attr, do it on the output.
// The key insight: the output TokenStream already contains the item.
// set_proc_macro_attr is typically called by the framework (serde_derive),
// not by user code directly. If you're calling it manually, reconsider.
// For manual cases where attribute transfer is needed:
let output_ts = proc_macro2::TokenStream::from(output.clone());
// ⚠️ The correct pattern is: produce output, then let the framework
// handle attribute propagation. Direct set_proc_macro_attr usage is rare.
output.into()
}
Fix 4: Check for conflicting proc-macro2 or proc-macro versions
If your project mixes proc-macro crate versions, ensure consistency:
cargo update proc-macro2
cargo update proc-macro
4. Verification
After applying the fix, verify that the error is resolved:
cargo build
A successful build produces no E0798 errors:
Compiling my_crate v0.1.0 (file:///path/to/my_crate)
Finished `dev` profile [dev dependencies]
1 target compiled in 0.42s
If using #[derive(Serialize, Deserialize)], verify serde integration works correctly:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Config {
host: String,
port: u16,
}
fn main() {
let cfg = Config {
host: "localhost".to_string(),
port: 8080,
};
let json = serde_json::to_string(&cfg).unwrap();
println!("{}", json);
}
Run the example:
cargo run --example config_example
Output:
{"host":"localhost","port":8080}
Verify the derive macro expanded correctly using cargo expand:
cargo install cargo-expand 2>/dev/null
cargo expand --lib
Look for the generated impl blocks in the expanded output. If they appear, the derive macro is functioning correctly.
5. Common Pitfalls
Pitfall 1: Returning the input token stream unchanged
Many derive macro implementations accidentally return TokenStream::new() or the unmodified input:
#[proc_macro_derive(BrokenDerive)]
pub fn broken_derive(input: TokenStream) -> TokenStream {
// ❌ Forgetting to return generated code
// Returning nothing or just the input causes issues
input // This might not produce the required output item
}
Fix: Always produce new tokens with quote! or equivalent:
#[proc_macro_derive(FixedDerive)]
pub fn fixed_derive(input: TokenStream) -> TokenStream {
let derive_input = syn::parse_macro_input!(input as DeriveInput);
let name = &derive_input.ident;
quote! {
impl FixedDerive for #name {}
}.into()
}
Pitfall 2: Using outdated serde with a new Rust toolchain
The Rust compiler’s proc-macro rules have tightened over time. Using serde_derive 1.0.100 with Rust 1.78+ can trigger E0798:
# Check your Rust version
rustc --version
# rustc 1.78.0 (stable)
# Check serde_derive version
cargo tree | grep serde_derive
# serde_derive v1.0.100 (registry `crates-io`)
Fix: Always pair modern Rust with an up-to-date serde:
# Cargo.toml
[package]
rust-version = "1.75"
[dependencies]
serde = { version = "1.0.215", features = ["derive"] }
Pitfall 3: Calling set_proc_macro_attr in the wrong phase
The function must be called after emitting the output token stream, not before or during parsing:
// ❌ Wrong: calling it at the start, before knowing the output
#[proc_macro_derive(BadDerive)]
pub fn bad_derive(input: TokenStream) -> TokenStream {
let _ = proc_macro::set_proc_macro_attr(
&TokenTree::Group(Group::new(Delimiter::Parenthesis, input.clone())),
|| {},
);
// Now trying to emit output — too late, E0798 already triggered
let derive_input = syn::parse_macro_input!(input as DeriveInput);
quote! { /* ... */ }.into()
}
Fix: Reverse the order — parse, generate, then return. The attribute propagation is handled by the macro framework, not by manually calling set_proc_macro_attr.
Pitfall 4: Mismatched proc-macro2 features
If proc-macro2 is built with the proc-macro feature disabled, some attribute-setting behaviors may not work correctly:
# Cargo.toml
[dependencies]
proc-macro2 = { version = "1.0", features = ["proc-macro"] }
Ensure the feature is enabled when writing procedural macros.
Pitfall 5: Multiple derive macros on the same item
When combining multiple derive macros where one uses set_proc_macro_attr, ensure each macro handles its own output correctly:
// ✅ Correct: each derive handles its own output
#[derive(Serialize, Deserialize, MyCustomDerive)]
struct MyStruct {
value: i32,
}
6. Related Errors
| Error Code | Description |
|---|---|
| E0277 | The trait bound X: Serialize is not satisfied — often caused by derive macro failure, which can be a downstream effect of E0798 preventing code generation. |
| E0433 | Failed to compile proc-macro due to errors in the macro definition — if the derive macro has internal errors, this may surface as E0433. |
| E0580 | Main function has the wrong type — unrelated to E0798, but may appear if macro expansion fails to generate the expected main function. |
| E0412 | Undefined name X in macro expansion — can occur when derive macro fails to generate required impl blocks. |
| E0769 | await is not allowed in this context — unrelated; proc-macro compilation errors sometimes mask other issues. |