Repository: GREsau/schemars
Pull request #410 (2025-05-30 21:02:17)

Fail compilation when encountering `with` container attribute
The `#[schemars(with = "...")]` attribute is not allowed on containers, but did not fail compilation, it was just silently ignored. This may mislead users into thinking that it was being processed.

Similarly, the `#[schemars(serialize_with = "...")]` attribute is never allowed, but also did not fail compilation.


Patch:
diff --git a/schemars_derive/src/attr/doc.rs b/schemars_derive/src/attr/doc.rs
index 778d493..1d126d5 100644
--- a/schemars_derive/src/attr/doc.rs
+++ b/schemars_derive/src/attr/doc.rs
@@ -1,16 +1,33 @@
 use proc_macro2::TokenStream;
-use quote::TokenStreamExt;
-use syn::Attribute;
+use quote::ToTokens;
+use syn::{Attribute, Expr, ExprLit, Lit};
 
-pub fn get_doc(attrs: &[Attribute]) -> Option<syn::Expr> {
+pub fn get_doc(attrs: &[Attribute]) -> Option<Expr> {
     let mut macro_args: TokenStream = TokenStream::new();
 
-    let lines = attrs
+    for (i, line) in attrs
         .iter()
         .filter(|a| a.path().is_ident("doc"))
         .flat_map(|a| a.meta.require_name_value())
-        .map(|m| &m.value);
-    macro_args.append_separated(lines, quote!(, "\n",));
+        .enumerate()
+    {
+        if i > 0 {
+            macro_args.extend([quote!(, "\n",)]);
+        }
+
+        if let Expr::Lit(ExprLit {
+            lit: Lit::Str(lit_str),
+            ..
+        }) = &line.value
+        {
+            if let Some(trimmed) = lit_str.value().strip_prefix(' ') {
+                trimmed.to_tokens(&mut macro_args);
+                continue;
+            }
+        }
+
+        line.value.to_tokens(&mut macro_args);
+    }
 
     if macro_args.is_empty() {
         None
diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs
index 1342130..cf70ea9 100644
--- a/schemars_derive/src/attr/mod.rs
+++ b/schemars_derive/src/attr/mod.rs
@@ -463,7 +463,7 @@ impl<'a> AttrCtxt<'a> {
 impl Drop for AttrCtxt<'_> {
     fn drop(&mut self) {
         if self.attr_type == "schemars" {
-            for unhandled_meta in self.metas.iter().filter(|m| !is_known_serde_keyword(m)) {
+            for unhandled_meta in self.metas.iter().filter(|m| !is_schemars_serde_keyword(m)) {
                 self.error_spanned_by(
                     unhandled_meta.path(),
                     format_args!(
@@ -476,8 +476,8 @@ impl Drop for AttrCtxt<'_> {
     }
 }
 
-fn is_known_serde_keyword(meta: &Meta) -> bool {
-    let known_keywords = schemars_to_serde::SERDE_KEYWORDS;
+fn is_schemars_serde_keyword(meta: &Meta) -> bool {
+    let known_keywords = schemars_to_serde::SCHEMARS_KEYWORDS_PARSED_BY_SERDE;
     meta.path()
         .get_ident()
         .map(|i| known_keywords.contains(&i.to_string().as_str()))
diff --git a/schemars_derive/src/attr/schemars_to_serde.rs b/schemars_derive/src/attr/schemars_to_serde.rs
index 4a09cc2..03bf334 100644
--- a/schemars_derive/src/attr/schemars_to_serde.rs
+++ b/schemars_derive/src/attr/schemars_to_serde.rs
@@ -37,6 +37,10 @@ pub(crate) static SERDE_KEYWORDS: &[&str] = &[
     "with",
 ];
 
+pub(crate) static SCHEMARS_KEYWORDS_PARSED_BY_SERDE: &[&str] =
+    // exclude "serialize_with" and "with"
+    SERDE_KEYWORDS.split_at(SERDE_KEYWORDS.len() - 2).0;
+
 // If a struct/variant/field has any #[schemars] attributes, then create copies of them
 // as #[serde] attributes so that serde_derive_internals will parse them for us.
 pub fn process_serde_attrs(input: &mut syn::DeriveInput) -> syn::Result<()> {
@@ -76,7 +80,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
             .into_iter()
             .filter_map(|meta| {
                 let keyword = get_meta_ident(&meta)?;
-                if SERDE_KEYWORDS.contains(&keyword.as_ref()) && !keyword.ends_with("with") {
+                if SCHEMARS_KEYWORDS_PARSED_BY_SERDE.contains(&keyword.as_ref()) {
                     Some((meta, keyword))
                 } else {
                     None


Test patch:
diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/docs.rs~doc_comments_enum.de.json b/schemars/tests/integration/snapshots/schemars/tests/integration/docs.rs~doc_comments_enum.de.json
index 0da3bed..aa6d047 100644
--- a/schemars/tests/integration/snapshots/schemars/tests/integration/docs.rs~doc_comments_enum.de.json
+++ b/schemars/tests/integration/snapshots/schemars/tests/integration/docs.rs~doc_comments_enum.de.json
@@ -1,7 +1,7 @@
 {
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "title": "This is the enum's title",
-  "description": "This is \n the enum's description.",
+  "description": "This is \nthe enum's description.",
   "oneOf": [
     {
       "type": "string",
@@ -25,7 +25,7 @@
           "properties": {
             "my_nullable_string": {
               "title": "A nullable string",
-              "description": "This field is a nullable string.\n\n This\nis\n   the second\n  line!\n\n\n\n\n And this is the third!",
+              "description": "This field is a nullable string.\n\nThis\nis\n  the second\n line!\n\n\n\n\nAnd this is the third!",
               "type": [
                 "string",
                 "null"
diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/docs.rs~doc_comments_enum.ser.json b/schemars/tests/integration/snapshots/schemars/tests/integration/docs.rs~doc_comments_enum.ser.json
index 3c6e680..6341d24 100644
--- a/schemars/tests/integration/snapshots/schemars/tests/integration/docs.rs~doc_comments_enum.ser.json
+++ b/schemars/tests/integration/snapshots/schemars/tests/integration/docs.rs~doc_comments_enum.ser.json
@@ -1,7 +1,7 @@
 {
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "title": "This is the enum's title",
-  "description": "This is \n the enum's description.",
+  "description": "This is \nthe enum's description.",
   "oneOf": [
     {
       "type": "string",
@@ -25,7 +25,7 @@
           "properties": {
             "my_nullable_string": {
               "title": "A nullable string",
-              "description": "This field is a nullable string.\n\n This\nis\n   the second\n  line!\n\n\n\n\n And this is the third!",
+              "description": "This field is a nullable string.\n\nThis\nis\n  the second\n line!\n\n\n\n\nAnd this is the third!",
               "type": [
                 "string",
                 "null"
diff --git a/schemars/tests/ui/invalid_attrs.rs b/schemars/tests/ui/invalid_attrs.rs
index dc6a689..883f63d 100644
--- a/schemars/tests/ui/invalid_attrs.rs
+++ b/schemars/tests/ui/invalid_attrs.rs
@@ -8,9 +8,14 @@ use schemars::JsonSchema;
     deny_unknown_fields,
     inline = 1,
     inline,
-    inline
+    inline,
+    with = "String",
+    serialize_with = "String"
 )]
-pub struct Struct1;
+pub struct Struct1 {
+    #[serde(serialize_with = "u64")]
+    pub field: u32,
+}
 
 #[derive(JsonSchema)]
 #[schemars(
@@ -20,8 +25,13 @@ pub struct Struct1;
     deny_unknown_fields,
     inline = 1,
     inline,
-    inline
+    inline,
+    with = "String",
+    serialize_with = "String"
 )]
-pub struct Struct2;
+pub struct Struct2 {
+    #[schemars(serialize_with = "u64")]
+    pub field: u32,
+}
 
 fn main() {}
diff --git a/schemars/tests/ui/invalid_attrs.stderr b/schemars/tests/ui/invalid_attrs.stderr
index 9c8a3c7..e46d0e6 100644
--- a/schemars/tests/ui/invalid_attrs.stderr
+++ b/schemars/tests/ui/invalid_attrs.stderr
@@ -10,32 +10,56 @@ error: duplicate serde attribute `deny_unknown_fields`
 8 |     deny_unknown_fields,
   |     ^^^^^^^^^^^^^^^^^^^
 
+error: unknown serde container attribute `with`
+  --> tests/ui/invalid_attrs.rs:12:5
+   |
+12 |     with = "String",
+   |     ^^^^
+
 error: expected serde default attribute to be a string: `default = "..."`
-  --> tests/ui/invalid_attrs.rs:17:15
+  --> tests/ui/invalid_attrs.rs:22:15
    |
-17 |     default = 0,
+22 |     default = 0,
    |               ^
 
 error: duplicate serde attribute `deny_unknown_fields`
-  --> tests/ui/invalid_attrs.rs:20:5
+  --> tests/ui/invalid_attrs.rs:25:5
    |
-20 |     deny_unknown_fields,
+25 |     deny_unknown_fields,
    |     ^^^^^^^^^^^^^^^^^^^
 
+error: unknown schemars attribute `serialize_with`
+  --> tests/ui/invalid_attrs.rs:33:16
+   |
+33 |     #[schemars(serialize_with = "u64")]
+   |                ^^^^^^^^^^^^^^
+
 error: unexpected value of schemars inline attribute item
-  --> tests/ui/invalid_attrs.rs:21:12
+  --> tests/ui/invalid_attrs.rs:26:12
    |
-21 |     inline = 1,
+26 |     inline = 1,
    |            ^^^
 
 error: duplicate schemars attribute item `inline`
-  --> tests/ui/invalid_attrs.rs:23:5
+  --> tests/ui/invalid_attrs.rs:28:5
    |
-23 |     inline
+28 |     inline,
    |     ^^^^^^
 
 error: unknown schemars attribute `foo`
-  --> tests/ui/invalid_attrs.rs:18:5
+  --> tests/ui/invalid_attrs.rs:23:5
    |
-18 |     foo,
+23 |     foo,
    |     ^^^
+
+error: unknown schemars attribute `with`
+  --> tests/ui/invalid_attrs.rs:29:5
+   |
+29 |     with = "String",
+   |     ^^^^
+
+error: unknown schemars attribute `serialize_with`
+  --> tests/ui/invalid_attrs.rs:30:5
+   |
+30 |     serialize_with = "String"
+   |     ^^^^^^^^^^^^^^