Skip to content

Conversation

@aytey
Copy link
Contributor

@aytey aytey commented Aug 1, 2025

In the 2024 edition of Rust, serde's macros for serialize_with can lead to a temporary lifetime error such as:

error[E0716]: temporary value dropped while borrowed
 --> my-binary/src/main.rs:6:10
  |
6 | #[derive(MyDerive)]
  |          ^^^^^^^-
  |          |      |
  |          |      temporary value is freed at the end of this statement
  |          creates a temporary value which is freed while still in use
  |          borrow later used by call
  |          in this derive macro expansion
  |
 ::: /private/tmp/life/my-project/my-macro/src/lib.rs:6:1
  |
6 | pub fn my_derive(_input: TokenStream) -> TokenStream {
  | ---------------------------------------------------- in this expansion of `#[derive(MyDerive)]`
  |
  = note: consider using a `let` binding to create a longer lived value

This is because the macro code takes a reference to struct inside of a block, which then goes out of scope when serde passes it to a function.

To resolve this, we move the reference to outside of the block, to ensure that the lifetime extends into the function call.

In the 2024 edition of Rust, `serde`s macros for `serialize_with` can
lead to a temporary lifetime error such as:

```
error[E0716]: temporary value dropped while borrowed
 --> my-binary/src/main.rs:6:10
  |
6 | #[derive(MyDerive)]
  |          ^^^^^^^-
  |          |      |
  |          |      temporary value is freed at the end of this statement
  |          creates a temporary value which is freed while still in use
  |          borrow later used by call
  |          in this derive macro expansion
  |
 ::: /private/tmp/life/my-project/my-macro/src/lib.rs:6:1
  |
6 | pub fn my_derive(_input: TokenStream) -> TokenStream {
  | ---------------------------------------------------- in this expansion of `#[derive(MyDerive)]`
  |
  = note: consider using a `let` binding to create a longer lived value
```

This is because the macro code takes a reference to struct inside of a
block, which then goes out of scope when `serde` passes it to a
function.

To resolve this, we move the reference to outside of the block, to
ensure that the lifetime extends into the function call.

Signed-off-by: Andrew V. Teylu <andrew.teylu@vector.com>
@aytey
Copy link
Contributor Author

aytey commented Aug 1, 2025

For reference, a minimal example that can reproduce the problem is something like:

use serde::{Serialize, Serializer};

#[derive(Serialize)]
struct S {
    #[serde(serialize_with = "f")]
    x: u8,
}

fn f<T, S>(_: &T, s: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    s.serialize_unit()
}

which generates code that is (similar to):

use std::marker::PhantomData;

trait Error {}

fn takes_reference<T>(_: &T) {}

struct S {
    x: u8,
}

impl S {
    fn method(&self) {
        takes_reference(
            {
                struct Wrapper<'a> {
                    value: &'a u8,
                    phantom: PhantomData<S>,
                }
                &Wrapper {
                    value: &self.x,
                    phantom: PhantomData,
                }
            }
        );
    }
}

The issue is that the reference is taken against Wrapper inside the block {}, but the temporary value's lifetime expires before being passed to takes_reference. The fix moves the reference outside the block.

In actual serde code, the function is serialize_field and the structure is __SerializeWith, but the bug is identical.

If you compile this with --edition 2024, you get:

error[E0716]: temporary value dropped while borrowed
  --> unit.rs:19:18
   |
13 |           takes_reference(
   |           --------------- borrow later used by call
...
19 |                   &Wrapper {
   |  __________________^
20 | |                     value: &self.x,
21 | |                     phantom: PhantomData,
22 | |                 }
   | |                 ^
   | |                 |
   | |_________________temporary value is freed at the end of this statement
   |                   creates a temporary value which is freed while still in use

Swapping all of serde to use 2024 (along with the minimal example I gave previously), gives:

error[E0716]: temporary value dropped while borrowed
 --> my-binary/src/main.rs:6:10
  |
6 | #[derive(MyDerive)]
  |          ^^^^^^^-
  |          |      |
  |          |      temporary value is freed at the end of this statement
  |          creates a temporary value which is freed while still in use
  |          borrow later used by call
  |          in this derive macro expansion
  |
 ::: /private/tmp/life/my-project/my-macro/src/lib.rs:6:1
  |
6 | pub fn my_derive(_input: TokenStream) -> TokenStream {
  | ---------------------------------------------------- in this expansion of `#[derive(MyDerive)]`
  |
  = note: consider using a `let` binding to create a longer lived value

Copy link
Member

@dtolnay dtolnay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

This should not make a difference to anyone using the Serialize derive macro because those macro-generated tokens are all spanned with serde_derive's edition, which is 2021. But this PR helps when someone copied macro-generated Serialize code from cargo expand into a 2024-edition crate.

@dtolnay dtolnay merged commit bb58726 into serde-rs:master Sep 14, 2025
12 of 14 checks passed
@aytey aytey deleted the fix_lifetime_issue_2024 branch September 14, 2025 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants