Combining optional dependencies and derive helper attributes

Specifically, I want to do something like this:

Cargo.toml

[features]
serde = ["dep:serde", "dep:serde_with"]

[dependencies]
serde = { version = "1.0.136", optional = true }
serde_with = { version = "1.12.0", optional = true }

lib.rs

#[cfg_attr(feature = "serde", serde_with::serde_as)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Test {
    #[cfg_attr(feature = "serde", serde_as(as = "serde_with::DisplayFromStr"))]
    pub test: Box<dyn std::fmt::Display>,
}

However, compiling with the feature fails:

D:\repos\cad97\playground> cargo check
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
D:\repos\cad97\playground> cargo check --features serde
    Checking playground v0.1.0 (D:\repos\cad97\playground)
error[E0277]: the trait bound `dyn std::fmt::Display: Serialize` is not satisfied
    --> src\lib.rs:4:5
     |
4    |     #[cfg_attr(feature = "serde", serde_as(as = "serde_with::DisplayFromStr"))]
     |     ^ the trait `Serialize` is not implemented for `dyn std::fmt::Display`
     |
     = note: required because of the requirements on the impl of `Serialize` for `Box<dyn std::fmt::Display>`
note: required by a bound in `_serde::ser::SerializeStruct::serialize_field`
    --> D:\.rust\cargo\registry\src\github.com-1ecc6299db9ec823\serde-1.0.136\src\ser\mod.rs:1899:12
     |
1899 |         T: Serialize;
     |            ^^^^^^^^^ required by this bound in `_serde::ser::SerializeStruct::serialize_field`

For more information about this error, try `rustc --explain E0277`.

If I change the helper attribute to #[serde_as(as = "serde_with::DisplayFromStr")], then compilation with the feature works, but compilation without the feature fails:

D:\repos\cad97\playground> cargo check --features serde
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
D:\repos\cad97\playground> cargo check
    Checking playground v0.1.0 (D:\repos\cad97\playground)
error: cannot find attribute `serde_as` in this scope
 --> src\lib.rs:4:7
  |
4 |     #[serde_as(as = "serde_with::DisplayFromStr")]
  |       ^^^^^^^^

error: could not compile `playground` due to previous error

Making this more frustrating is the fact that the #[serde_as] attribute shows up in cargo expand:

D:\repos\cad97\playground> cargo expand --features serde
    Checking playground v0.1.0 (D:\repos\cad97\playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.17s

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
pub struct Test {
    #[serde_as(as = "serde_with::DisplayFromStr")]
    pub test: Box<dyn std::fmt::Display>,
}
#[doc(hidden)]
#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
const _: () = {
    #[allow(unused_extern_crates, clippy::useless_attribute)]
    extern crate serde as _serde;
    #[automatically_derived]
    impl _serde::Serialize for Test {
        // snip
    }
};

This must be some interaction between the multiple cfg_attr gated attributes, as cfg_attr gating helper attributes seems to work with just a single gated item attribute:

#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Test {
    #[cfg_attr(feature = "serde", serde(with = "serde_with::rust::display_fromstr"))]
    pub test: Box<dyn std::fmt::Display>,
}

(My use case actually does require the increased versatility of #[serde_as] versus #[serde(with)].)

This occurs even if putting both attributes in the same cfg_attr container (i.e. #[cfg_attr(feature = "serde", serde_with::serde_as, derive(serde::Serialize))]).

Is there some way to make this work, or do I have to resort to just not using the #[serde_as] helper and writing what it expands to manually?

#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Test {
    #[cfg_attr(
        feature = "serde",
        serde(with = "serde_with::As::<serde_with::DisplayFromStr>")
    )]
    pub test: Box<dyn std::fmt::Display>,
}

This is currently your best option. The serde_as documentation lists which transformation steps are performed. You can find some more information here `serde_as` cannot be cfg-gated · Discussion #331 · jonasbb/serde_with · GitHub. Unfortunately, fixing this properly, I think requires new rustc features or forking serde_derive.


Let's back up a moment. The problem is that the serde_as macro looks for more serde_as attributes inside the struct/enum. It does not find any, since they are nested inside cfg_attr. I am not aware of a way to recursively parse cfg expressions and evaluate them inside the context of a proc-macro. So serde_as does not even attempt to process them.

One "solution" would be to remove all cfg-gates from all serde_as attributes. This would allow you to use serde_as unencumbered, but would force you into an unconditional dependency to serde. So this is probably not wanted. (Note: This doesn't quite work yet, but would be easy to achieve).

As I said, the problem is processing all cfg-gates. One solution would be for rustc to remove them before serde_as runs. This is my understanding of what cfg_eval is set out to achieve (Tracking Issue for built-in attribute macro `#[cfg_eval]` · Issue #82679 · rust-lang/rust · GitHub), but unfortunately, there were two PRs attempting to stabilize it (Stabilize `#[cfg_eval]` and `feature(macro_attributes_in_derive_output)` by petrochenkov · Pull Request #83824 · rust-lang/rust · GitHub, Stabilize built-in attribute macro `#[cfg_eval]` by petrochenkov · Pull Request #87221 · rust-lang/rust · GitHub) and it seems not to be moving anywhere.

The other option is using a derive macro, which always have the cfg values evaluated. This would require forking serde_derive and parts of serde. That would allow a nicer integration and maybe also fix some serde_derive shortcomings, but it seemed too daunting of a task for unknown gain.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.