Repository: jonasbb/serde_with
Pull request #782 (2024-09-21 14:42:49)

feat(serde_with_macros): proper handling of `cfg_attr` and `schemars`
With this commit, `cfg_attr` will be handled properly. The current implementation does not accept e.g.:

```rust
#[serde_as]
#[derive(JsonSchema, Serialize)]
struct Test {
    #[serde_as(as = "DisplayFromStr")]
    #[cfg_attr(any(), arbitrary("some" |weird| syntax::<bool, 2>()))]
    #[cfg_attr(any(), schemars(rename = "foo"), schemars(range(min = 0)))]
    custom: i32,
}
```

Because due to https://github.com/jonasbb/serde_with/blob/master/serde_with_macros/src/utils.rs#L91, the parser does not accept `cfg_attr` with multiple arguments, which is allowed, see e.g. https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute. Another issue I’ve found with `arbitrary` where some unexpected errors due to this custom parser: https://github.com/jonasbb/serde_with/blob/master/serde_with_macros/src/utils.rs#L191-L218 in combination with this line: https://github.com/jonasbb/serde_with/blob/master/serde_with_macros/src/utils.rs#L240.

Line 240 will only `continue`, if the outer-most meta if neither `cfg_attr` or `schemars`. If however the attribute is `cfg_attr`, the code after the `match`-block is reached and the parser will just assume, that the inner meta is `schemars` and thus fail to parse any meta with some weird syntax.

Even `#[schemars(length(min = 1.0))]` will fail, because the parser expects all `schemars`-attributes to be a list of key-value-pairs (`foo = bar`), thus failing for `length(min = 1.0)`, even though this is perfectly legal (https://graham.cool/schemars/deriving/attributes/#length).

I introduced the type `LazyBool` to simplify dealing with various conditions.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/jonasbb/serde_with/pull/782?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 67.34%. Comparing base [(`378fa24`)](https://app.codecov.io/gh/jonasbb/serde_with/commit/378fa24e68f106aec8bdc36fbdf41897e2138610?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) to head [(`13b65ab`)](https://app.codecov.io/gh/jonasbb/serde_with/commit/13b65ab406da41724c7e5ef76dbfbf3d258cdbaa?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart).
> Report is 6 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #782      +/-   ##
==========================================
- Coverage   67.45%   67.34%   -0.11%     
==========================================
  Files          40       40              
  Lines        2464     2468       +4     
==========================================
  Hits         1662     1662              
- Misses        802      806       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/jonasbb/serde_with/pull/782?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart).


Patch:
diff --git a/serde_with_macros/src/lazy_bool.rs b/serde_with_macros/src/lazy_bool.rs
new file mode 100644
index 0000000..434d58b
--- /dev/null
+++ b/serde_with_macros/src/lazy_bool.rs
@@ -0,0 +1,139 @@
+use core::{
+    mem,
+    ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not},
+};
+
+/// Not-yet evaluated boolean value.
+#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub enum LazyBool<T> {
+    /// Like `false`.
+    ///
+    /// This is this type’s default.
+    #[default]
+    False,
+
+    /// Like `true`.
+    True,
+
+    /// Not-yet decided.
+    Lazy(T),
+}
+
+impl<T> From<bool> for LazyBool<T> {
+    fn from(value: bool) -> Self {
+        match value {
+            false => Self::False,
+            true => Self::True,
+        }
+    }
+}
+
+/// Helper to implement various binary operations on [`LazyBool`].
+macro_rules! impl_op {
+    (
+        <
+            $trait:ident::$method:ident,
+            $assign_trait:ident::$assign_method:ident
+        >($matching:pat_param) {
+            $($pattern:pat => $body:expr),+ $(,)?
+        }
+        $(where $($bound:tt)+)?
+    ) => {
+        impl<L, R, T> $trait<LazyBool<R>> for LazyBool<L>
+        where
+            L: $trait<R, Output = T>,
+            LazyBool<L>: Into<LazyBool<T>>,
+            LazyBool<R>: Into<LazyBool<T>>,
+            $($($bound)+)?
+        {
+            type Output = LazyBool<T>;
+
+            fn $method(self, rhs: LazyBool<R>) -> Self::Output {
+                match (self, rhs) {
+                    (LazyBool::Lazy(lhs), LazyBool::Lazy(rhs)) => LazyBool::Lazy(lhs.$method(rhs)),
+                    ($matching, rhs) => rhs.into(),
+                    (lhs, $matching) => lhs.into(),
+                    $($pattern => $body),+
+                }
+            }
+        }
+
+        impl<'a, L, R, T> $trait<&'a LazyBool<R>> for LazyBool<L>
+        where
+            L: $trait<&'a R, Output = T>,
+            LazyBool<L>: Into<LazyBool<T>>,
+            LazyBool<R>: Into<LazyBool<T>> + Clone,
+            $($($bound)+)?
+        {
+            type Output = LazyBool<T>;
+
+            fn $method(self, rhs: &'a LazyBool<R>) -> Self::Output {
+                match (self, rhs) {
+                    (LazyBool::Lazy(lhs), LazyBool::Lazy(rhs)) => LazyBool::Lazy(lhs.$method(rhs)),
+                    ($matching, rhs) => rhs.clone().into(),
+                    (lhs, $matching) => lhs.into(),
+                    $($pattern => $body),+
+                }
+            }
+        }
+
+        impl<'a, L, R, T> $trait<LazyBool<R>> for &'a LazyBool<L>
+        where
+            LazyBool<R>: $trait<&'a LazyBool<L>, Output = LazyBool<T>>,
+        {
+            type Output = LazyBool<T>;
+
+            fn $method(self, rhs: LazyBool<R>) -> Self::Output {
+                rhs.$method(self)
+            }
+        }
+
+        impl<L, R> $assign_trait<LazyBool<R>> for LazyBool<L>
+        where
+            LazyBool<L>: $trait<LazyBool<R>, Output = LazyBool<L>>,
+        {
+            fn $assign_method(&mut self, rhs: LazyBool<R>) {
+                let lhs = mem::take(self);
+                *self = lhs.$method(rhs);
+            }
+        }
+    };
+}
+
+impl_op! { <BitAnd::bitand, BitAndAssign::bitand_assign>(LazyBool::True){ _ => LazyBool::False } }
+impl_op! { <BitOr::bitor, BitOrAssign::bitor_assign>(LazyBool::False) { _ => LazyBool::True } }
+impl_op! {
+    <BitXor::bitxor, BitXorAssign::bitxor_assign>(LazyBool::False) {
+        (LazyBool::True, rhs) => (!rhs).into(),
+        (lhs, LazyBool::True) => (!lhs).into(),
+    }
+    where
+        LazyBool<L>: Not<Output = LazyBool<L>>,
+        LazyBool<R>: Not<Output = LazyBool<R>>,
+}
+
+impl<T> Not for LazyBool<T>
+where
+    T: Not<Output = T>,
+{
+    type Output = Self;
+
+    fn not(self) -> Self::Output {
+        match self {
+            Self::False => Self::True,
+            Self::True => Self::False,
+            Self::Lazy(this) => Self::Lazy(!this),
+        }
+    }
+}
+
+impl<T> Not for &LazyBool<T>
+where
+    LazyBool<T>: Not<Output = LazyBool<T>> + Clone,
+{
+    type Output = LazyBool<T>;
+
+    fn not(self) -> Self::Output {
+        !self.clone()
+    }
+}
diff --git a/serde_with_macros/src/lib.rs b/serde_with_macros/src/lib.rs
index 5f4d998..79c63c2 100644
--- a/serde_with_macros/src/lib.rs
+++ b/serde_with_macros/src/lib.rs
@@ -32,9 +32,13 @@
 //! [`serde_with`]: https://crates.io/crates/serde_with/
 
 mod apply;
+mod lazy_bool;
 mod utils;
 
-use crate::utils::{split_with_de_lifetime, DeriveOptions, IteratorExt as _, SchemaFieldConfig};
+use crate::utils::{
+    split_with_de_lifetime, DeriveOptions, IteratorExt as _, SchemaFieldCondition,
+    SchemaFieldConfig,
+};
 use darling::{
     ast::NestedMeta,
     util::{Flag, Override},
@@ -619,10 +623,9 @@ pub fn serde_as(args: TokenStream, input: TokenStream) -> TokenStream {
                 .unwrap_or_else(|| syn::parse_quote!(::serde_with));
 
             let schemars_config = match container_options.enable_schemars_support {
-                _ if cfg!(not(feature = "schemars_0_8")) => SchemaFieldConfig::Disabled,
-                Some(true) => SchemaFieldConfig::Unconditional,
-                Some(false) => SchemaFieldConfig::Disabled,
-                None => utils::has_derive_jsonschema(input.clone()),
+                _ if cfg!(not(feature = "schemars_0_8")) => SchemaFieldConfig::False,
+                Some(condition) => condition.into(),
+                None => utils::has_derive_jsonschema(input.clone()).unwrap_or_default(),
             };
 
             // Convert any error message into a nice compiler error
@@ -772,21 +775,40 @@ fn serde_as_add_attr_to_field(
         let attr = parse_quote!(#[serde(with = #attr_inner_tokens)]);
         field.attrs.push(attr);
 
-        if let Some(cfg) = schemars_config.cfg_expr() {
-            let with_cfg = utils::schemars_with_attr_if(
-                &field.attrs,
-                &["with", "serialize_with", "deserialize_with", "schema_with"],
-            )?;
-            let attr_inner_tokens =
-                quote!(#serde_with_crate_path::Schema::<#type_original, #replacement_type>)
-                    .to_string();
-            let attr = parse_quote! {
-                #[cfg_attr(
-                    all(#cfg, not(#with_cfg)),
-                    schemars(with = #attr_inner_tokens))
-                ]
-            };
-            field.attrs.push(attr);
+        match schemars_config {
+            SchemaFieldConfig::False => {}
+            lhs => {
+                let rhs = utils::schemars_with_attr_if(
+                    &field.attrs,
+                    &["with", "serialize_with", "deserialize_with", "schema_with"],
+                )?;
+
+                match lhs & !rhs {
+                    SchemaFieldConfig::False => {}
+                    condition => {
+                        let attr_inner_tokens = quote! {
+                            #serde_with_crate_path::Schema::<#type_original, #replacement_type>
+                        };
+                        let attr_inner_tokens = attr_inner_tokens.to_string();
+                        let attr = match condition {
+                            SchemaFieldConfig::False => unreachable!(),
+                            SchemaFieldConfig::True => {
+                                parse_quote! { #[schemars(with = #attr_inner_tokens)] }
+                            }
+                            SchemaFieldConfig::Lazy(SchemaFieldCondition(condition)) => {
+                                parse_quote! {
+                                    #[cfg_attr(
+                                        #condition,
+                                        schemars(with = #attr_inner_tokens))
+                                    ]
+                                }
+                            }
+                        };
+
+                        field.attrs.push(attr);
+                    }
+                }
+            }
         }
     }
     if let Some(type_) = &serde_as_options.deserialize_as {
diff --git a/serde_with_macros/src/utils.rs b/serde_with_macros/src/utils.rs
index fe0696e..4bc08cb 100644
--- a/serde_with_macros/src/utils.rs
+++ b/serde_with_macros/src/utils.rs
@@ -1,18 +1,21 @@
+use crate::lazy_bool::LazyBool;
 use darling::FromDeriveInput;
 use proc_macro::TokenStream;
-use proc_macro2::{TokenStream as TokenStream2, TokenTree};
+use proc_macro2::TokenStream as TokenStream2;
 use quote::ToTokens;
-use std::collections::BTreeSet;
+use std::ops::{BitAnd, BitOr, Not};
 use syn::{
-    ext::IdentExt, parse::Parse, parse_quote, punctuated::Punctuated, Error, Generics, Path,
-    TypeGenerics,
+    parse::{Parse, ParseStream},
+    parse_quote,
+    punctuated::Punctuated,
+    Attribute, DeriveInput, Generics, Meta, Path, PathSegment, Token, TypeGenerics, WhereClause,
 };
 
 /// Merge multiple [`syn::Error`] into one.
 pub(crate) trait IteratorExt {
-    fn collect_error(self) -> Result<(), Error>
+    fn collect_error(self) -> syn::Result<()>
     where
-        Self: Iterator<Item = Result<(), Error>> + Sized,
+        Self: Iterator<Item = syn::Result<()>> + Sized,
     {
         let accu = Ok(());
         self.fold(accu, |accu, error| match (accu, error) {
@@ -25,7 +28,7 @@ pub(crate) trait IteratorExt {
         })
     }
 }
-impl<I> IteratorExt for I where I: Iterator<Item = Result<(), Error>> + Sized {}
+impl<I> IteratorExt for I where I: Iterator<Item = syn::Result<()>> + Sized {}
 
 /// Attributes usable for derive macros
 #[derive(FromDeriveInput)]
@@ -37,7 +40,7 @@ pub(crate) struct DeriveOptions {
 }
 
 impl DeriveOptions {
-    pub(crate) fn from_derive_input(input: &syn::DeriveInput) -> Result<Self, TokenStream> {
+    pub(crate) fn from_derive_input(input: &DeriveInput) -> Result<Self, TokenStream> {
         match <Self as FromDeriveInput>::from_derive_input(input) {
             Ok(v) => Ok(v),
             Err(e) => Err(TokenStream::from(e.write_errors())),
@@ -55,11 +58,7 @@ impl DeriveOptions {
 // Serde is also licensed Apache 2 + MIT
 pub(crate) fn split_with_de_lifetime(
     generics: &Generics,
-) -> (
-    DeImplGenerics<'_>,
-    TypeGenerics<'_>,
-    Option<&syn::WhereClause>,
-) {
+) -> (DeImplGenerics<'_>, TypeGenerics<'_>, Option<&WhereClause>) {
     let de_impl_generics = DeImplGenerics(generics);
     let (_, ty_generics, where_clause) = generics.split_for_impl();
     (de_impl_generics, ty_generics, where_clause)
@@ -86,170 +85,174 @@ impl<'a> ToTokens for DeImplGenerics<'a> {
 ///            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 /// ```
 struct CfgAttr {
-    cfg: syn::Expr,
-    _comma: syn::Token![,],
-    meta: syn::Meta,
+    condition: Meta,
+    _comma: Token![,],
+    metas: Punctuated<Meta, Token![,]>,
 }
 
 impl Parse for CfgAttr {
-    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
+    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
         Ok(Self {
-            cfg: input.parse()?,
+            condition: input.parse()?,
             _comma: input.parse()?,
-            meta: input.parse()?,
+            metas: Punctuated::parse_terminated(input)?,
         })
     }
 }
 
 /// Determine if there is a `#[derive(JsonSchema)]` on this struct.
-pub(crate) fn has_derive_jsonschema(input: TokenStream) -> SchemaFieldConfig {
-    fn parse_derive_args(
-        input: syn::parse::ParseStream<'_>,
-    ) -> syn::Result<Punctuated<Path, syn::Token![,]>> {
+pub(crate) fn has_derive_jsonschema(input: TokenStream) -> syn::Result<SchemaFieldConfig> {
+    fn parse_derive_args(input: ParseStream<'_>) -> syn::Result<Punctuated<Path, Token![,]>> {
         Punctuated::parse_terminated_with(input, Path::parse_mod_style)
     }
 
-    fn eval_attribute(attr: &syn::Attribute) -> syn::Result<SchemaFieldConfig> {
-        let args: CfgAttr;
-        let mut meta = &attr.meta;
-        let mut config = SchemaFieldConfig::Unconditional;
-
-        if meta.path().is_ident("cfg_attr") {
-            let list = meta.require_list()?;
-            args = list.parse_args()?;
-
-            meta = &args.meta;
-            config = SchemaFieldConfig::Conditional(args.cfg);
-        }
-
-        let list = meta.require_list()?;
-        if !list.path.is_ident("derive") {
-            return Ok(SchemaFieldConfig::Disabled);
-        }
+    fn eval_metas<'a>(metas: impl IntoIterator<Item = &'a Meta>) -> syn::Result<SchemaFieldConfig> {
+        metas
+            .into_iter()
+            .map(eval_meta)
+            .try_fold(
+                SchemaFieldConfig::False,
+                |state, result| Ok(state | result?),
+            )
+    }
 
-        let derives = list.parse_args_with(parse_derive_args)?;
-        for derive in &derives {
-            let segments = &derive.segments;
+    fn eval_meta(meta: &Meta) -> syn::Result<SchemaFieldConfig> {
+        match meta.path() {
+            path if path.is_ident("cfg_attr") => {
+                let CfgAttr {
+                    condition, metas, ..
+                } = meta.require_list()?.parse_args()?;
 
-            // Check for $(::)? $(schemars::)? JsonSchema
-            match segments.len() {
-                1 if segments[0].ident == "JsonSchema" => (),
-                2 if segments[0].ident == "schemars" && segments[1].ident == "JsonSchema" => (),
-                _ => continue,
+                Ok(eval_metas(&metas)? & SchemaFieldConfig::Lazy(condition.into()))
             }
+            path if path.is_ident("derive") => {
+                let config = meta
+                    .require_list()?
+                    .parse_args_with(parse_derive_args)?
+                    .into_iter()
+                    .any(|Path { segments, .. }| {
+                        // This matches `JsonSchema`, `schemars::JsonSchema`
+                        //   as well as any other path ending with `JsonSchema`.
+                        // This will not match aliased `JsonSchema`s,
+                        //   but might match other `JsonSchema` not `schemars::JsonSchema`!
+                        match segments.last() {
+                            Some(PathSegment { ident, .. }) => ident == "JsonSchema",
+                            _ => false,
+                        }
+                    })
+                    .then_some(SchemaFieldConfig::True)
+                    .unwrap_or_default();
 
-            return Ok(config);
-        }
-
-        Ok(SchemaFieldConfig::Disabled)
-    }
-
-    let input: syn::DeriveInput = match syn::parse(input) {
-        Ok(input) => input,
-        Err(_) => return SchemaFieldConfig::Disabled,
-    };
-
-    for attr in input.attrs {
-        match eval_attribute(&attr) {
-            Ok(SchemaFieldConfig::Disabled) => continue,
-            Ok(config) => return config,
-            Err(_) => continue,
+                Ok(config)
+            }
+            _ => Ok(SchemaFieldConfig::False),
         }
     }
 
-    SchemaFieldConfig::Disabled
+    let DeriveInput { attrs, .. } = syn::parse(input)?;
+    let metas = attrs.iter().map(|Attribute { meta, .. }| meta);
+    eval_metas(metas)
 }
 
 /// Enum controlling when we should emit a `#[schemars]` field attribute.
-pub(crate) enum SchemaFieldConfig {
-    /// Emit a `#[cfg_attr(#cfg, schemars(...))]` attribute.
-    Conditional(syn::Expr),
-    /// Emit a `#[schemars(...)]` attribute (or equivalent).
-    Unconditional,
-    /// Do not emit an attribute.
-    Disabled,
+pub(crate) type SchemaFieldConfig = LazyBool<SchemaFieldCondition>;
+
+impl From<Meta> for SchemaFieldConfig {
+    fn from(meta: Meta) -> Self {
+        Self::Lazy(meta.into())
+    }
 }
 
-impl SchemaFieldConfig {
-    /// Get a `#[cfg]` expression suitable for emitting the `#[schemars]` attribute.
-    ///
-    /// If this config is `Unconditional` then it will just return `all()` which
-    /// is always true.
-    pub(crate) fn cfg_expr(&self) -> Option<syn::Expr> {
-        match self {
-            Self::Unconditional => Some(syn::parse_quote!(all())),
-            Self::Conditional(cfg) => Some(cfg.clone()),
-            Self::Disabled => None,
-        }
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]
+pub(crate) struct SchemaFieldCondition(pub(crate) Meta);
+
+impl BitAnd for SchemaFieldCondition {
+    type Output = Self;
+
+    fn bitand(self, Self(rhs): Self) -> Self::Output {
+        let Self(lhs) = self;
+        Self(parse_quote!(all(#lhs, #rhs)))
     }
 }
 
-struct SchemarsAttr {
-    args: BTreeSet<String>,
+impl BitAnd<&SchemaFieldCondition> for SchemaFieldCondition {
+    type Output = Self;
+
+    fn bitand(self, Self(rhs): &Self) -> Self::Output {
+        let Self(lhs) = self;
+        Self(parse_quote!(all(#lhs, #rhs)))
+    }
 }
 
-impl Parse for SchemarsAttr {
-    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
-        let mut args = BTreeSet::new();
+impl BitOr for SchemaFieldCondition {
+    type Output = Self;
 
-        while !input.is_empty() {
-            let arg = syn::Ident::parse_any(input)?;
-            let _eq: syn::Token![=] = input.parse()?;
+    fn bitor(self, Self(rhs): Self) -> Self::Output {
+        let Self(lhs) = self;
+        Self(parse_quote!(any(#lhs, #rhs)))
+    }
+}
 
-            args.insert(arg.to_string());
+impl Not for SchemaFieldCondition {
+    type Output = Self;
 
-            // Don't parse the argument value, just advance until we hit the end or a comma.
-            input.step(|cursor| {
-                let mut rest = *cursor;
-                loop {
-                    match rest.token_tree() {
-                        Some((TokenTree::Punct(punct), next)) if punct.as_char() == ',' => {
-                            return Ok(((), next))
-                        }
-                        Some((_, next)) => rest = next,
-                        None => return Ok(((), rest)),
-                    }
-                }
-            })?;
-        }
+    fn not(self) -> Self::Output {
+        let Self(condition) = self;
+        Self(parse_quote!(not(#condition)))
+    }
+}
 
-        Ok(Self { args })
+impl From<Meta> for SchemaFieldCondition {
+    fn from(meta: Meta) -> Self {
+        Self(meta)
     }
 }
 
 /// Get a `#[cfg]` expression under which this field has a `#[schemars]` attribute
 /// with a `with = ...` argument.
 pub(crate) fn schemars_with_attr_if(
-    attrs: &[syn::Attribute],
+    attrs: &[Attribute],
     filter: &[&str],
-) -> syn::Result<syn::Expr> {
-    let mut conditions = Vec::new();
-
-    for attr in attrs {
-        let path = attr.path();
+) -> syn::Result<SchemaFieldConfig> {
+    fn eval_metas<'a>(
+        filter: &[&str],
+        metas: impl IntoIterator<Item = &'a Meta>,
+    ) -> syn::Result<SchemaFieldConfig> {
+        metas
+            .into_iter()
+            .map(|meta| eval_meta(filter, meta))
+            .try_fold(
+                SchemaFieldConfig::False,
+                |state, result| Ok(state | result?),
+            )
+    }
 
-        let nested;
-        let (cfg, meta) = match () {
-            _ if path.is_ident("cfg_attr") => {
-                let cfg_attr = attr.parse_args_with(CfgAttr::parse)?;
-                nested = cfg_attr.meta;
+    fn eval_meta(filter: &[&str], meta: &Meta) -> syn::Result<SchemaFieldConfig> {
+        match meta.path() {
+            path if path.is_ident("cfg_attr") => {
+                let CfgAttr {
+                    condition, metas, ..
+                } = meta.require_list()?.parse_args()?;
 
-                (cfg_attr.cfg, &nested)
+                Ok(eval_metas(filter, &metas)? & SchemaFieldConfig::from(condition))
             }
-            _ if path.is_ident("schemars") => (syn::parse_quote!(all()), &attr.meta),
-            _ => continue,
-        };
-
-        let list = meta.require_list()?;
-        let schemars: SchemarsAttr = syn::parse2(list.tokens.clone())?;
-        let args = &schemars.args;
-
-        if !filter.iter().copied().any(|item| args.contains(item)) {
-            continue;
+            path if path.is_ident("schemars") => {
+                let config = meta
+                    .require_list()?
+                    .parse_args_with(<Punctuated<Meta, Token![,]>>::parse_terminated)?
+                    .into_iter()
+                    .any(|meta| match meta.path().get_ident() {
+                        Some(ident) => filter.iter().any(|relevant| ident == relevant),
+                        _ => false,
+                    })
+                    .then_some(SchemaFieldConfig::True)
+                    .unwrap_or_default();
+                Ok(config)
+            }
+            _ => Ok(SchemaFieldConfig::False),
         }
-
-        conditions.push(cfg);
     }
 
-    Ok(syn::parse_quote!(any(#( #conditions, )*)))
+    let metas = attrs.iter().map(|Attribute { meta, .. }| meta);
+    eval_metas(filter, metas)
 }


Test patch:
diff --git a/serde_with/tests/schemars_0_8.rs b/serde_with/tests/schemars_0_8.rs
index a6c923e..d8b4111 100644
--- a/serde_with/tests/schemars_0_8.rs
+++ b/serde_with/tests/schemars_0_8.rs
@@ -94,6 +94,22 @@ fn schemars_basic() {
     expected.assert_eq(&schema);
 }
 
+#[test]
+fn schemars_other_cfg_attrs() {
+    #[serde_as]
+    #[derive(JsonSchema, Serialize)]
+    struct Test {
+        #[serde_as(as = "DisplayFromStr")]
+        #[cfg_attr(any(), arbitrary("some" |weird| syntax::<bool, 2>()))]
+        #[cfg_attr(any(), schemars(with = "i32"))]
+        custom: i32,
+    }
+
+    check_matches_schema::<Test>(&json!({
+        "custom": "23",
+    }));
+}
+
 #[test]
 fn schemars_custom_with() {
     #[serde_as]