Repository: GREsau/schemars (rust)

Issue:
Validation with #[serde(transparent)]
I was excited to see the new validation arriving in 0.8.4, as I was hoping that I could get rid of some hand-written parts in my own code.  But I _think_ that currently it doesn't support my use case, so I'm raising this to (i) confirm that's true (ii) check whether you'd consider this to be a bug.

Let's say my API includes a list of strings, where each string should conform to some pattern.  Let's say, they should be numeric strings.

```json
{
   "numeric_strings": ["123", "234"]
}
```

How should I express this in schemars?  I was hoping to write this:

```rust
#[derive(Debug, JsonSchema)]
#[serde(transparent)]
pub struct NumericString(#[schemars(regex(pattern = r"^\d+$"))] String);

#[derive(Debug, JsonSchema)]
pub struct VecOfNumericStrings {
    numeric_strings: Vec<NumericStrings>,
}
```
But the generated schema does not use the pattern at all.

Did I miss a better way?

Thanks!

Pull request description:
When a struct is `transparent`, don't ignore other attributes
Fixes #107

With this PR, when a struct has `#[serde(transparent)]` and any* other attributes that affect schema generation (including doc comments), the resulting schema won't be identical to the inner type, but we will still handle the transparent serialization/deserialization contract appropriately (specifically, by treating the struct as though it were newtype).

This means that putting ` #[serde(transparent)]`/ `#[schemars(transparent)]` on a newtype that has any* other attributes is essentially a no-op, which is fine.

*excluding a few attributes ( e.g. `schema_with`) which were already respected when present on a transparent struct

Patch:
diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs
index 441a8d1..f16aeb2 100644
--- a/schemars_derive/src/attr/mod.rs
+++ b/schemars_derive/src/attr/mod.rs
@@ -150,7 +150,7 @@ impl CommonAttrs {
         None
     }
 
-    fn is_default(&self) -> bool {
+    pub fn is_default(&self) -> bool {
         matches!(
             self,
             Self {
@@ -264,6 +264,16 @@ impl FieldAttrs {
 
         None
     }
+
+    pub fn is_default(&self) -> bool {
+        matches!(
+            self,
+            Self {
+                common,
+                validation,
+                with: None,
+            } if common.is_default() && validation.is_default())
+    }
 }
 
 impl ContainerAttrs {
diff --git a/schemars_derive/src/attr/validation.rs b/schemars_derive/src/attr/validation.rs
index 6b2157a..07abfa7 100644
--- a/schemars_derive/src/attr/validation.rs
+++ b/schemars_derive/src/attr/validation.rs
@@ -230,4 +230,20 @@ impl ValidationAttrs {
             }
         }
     }
+
+    pub(crate) fn is_default(&self) -> bool {
+        matches!(
+            self,
+            Self {
+                contains: None,
+                format: None,
+                length: None,
+                range: None,
+                pattern: None,
+                regex: None,
+                required: false,
+                inner: None,
+            }
+        )
+    }
 }
diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs
index 6cc4505..bc85caa 100644
--- a/schemars_derive/src/lib.rs
+++ b/schemars_derive/src/lib.rs
@@ -50,40 +50,45 @@ fn derive_json_schema(mut input: syn::DeriveInput, repr: bool) -> syn::Result<To
     let (impl_generics, ty_generics, where_clause) = cont.generics.split_for_impl();
 
     if let Some(transparent_field) = cont.transparent_field() {
-        let (ty, type_def) = schema_exprs::type_for_field_schema(transparent_field);
-        return Ok(quote! {
-            const _: () = {
-                #crate_alias
-                #type_def
-
-                #[automatically_derived]
-                impl #impl_generics schemars::JsonSchema for #type_name #ty_generics #where_clause {
-                    fn inline_schema() -> bool {
-                        <#ty as schemars::JsonSchema>::inline_schema()
-                    }
-
-                    fn schema_name() -> schemars::_private::alloc::borrow::Cow<'static, str> {
-                        <#ty as schemars::JsonSchema>::schema_name()
-                    }
-
-                    fn schema_id() -> schemars::_private::alloc::borrow::Cow<'static, str> {
-                        <#ty as schemars::JsonSchema>::schema_id()
-                    }
-
-                    fn json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema {
-                        <#ty as schemars::JsonSchema>::json_schema(#GENERATOR)
-                    }
-
-                    fn _schemars_private_non_optional_json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema {
-                        <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#GENERATOR)
-                    }
-
-                    fn _schemars_private_is_option() -> bool {
-                        <#ty as schemars::JsonSchema>::_schemars_private_is_option()
-                    }
+        // If any schemars attributes for setting metadata (e.g. description) are present, then
+        // it's not fully transparent, so use the normal `schema_exprs::expr_for_container`
+        // implementation (which always treats the struct as a newtype if it has `transparent`)
+        if cont.attrs.common.is_default() && transparent_field.attrs.is_default() {
+            let (ty, type_def) = schema_exprs::type_for_field_schema(transparent_field);
+            return Ok(quote! {
+                const _: () = {
+                    #crate_alias
+                    #type_def
+
+                    #[automatically_derived]
+                    impl #impl_generics schemars::JsonSchema for #type_name #ty_generics #where_clause {
+                        fn inline_schema() -> bool {
+                            <#ty as schemars::JsonSchema>::inline_schema()
+                        }
+
+                        fn schema_name() -> schemars::_private::alloc::borrow::Cow<'static, str> {
+                            <#ty as schemars::JsonSchema>::schema_name()
+                        }
+
+                        fn schema_id() -> schemars::_private::alloc::borrow::Cow<'static, str> {
+                            <#ty as schemars::JsonSchema>::schema_id()
+                        }
+
+                        fn json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema {
+                            <#ty as schemars::JsonSchema>::json_schema(#GENERATOR)
+                        }
+
+                        fn _schemars_private_non_optional_json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema {
+                            <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#GENERATOR)
+                        }
+
+                        fn _schemars_private_is_option() -> bool {
+                            <#ty as schemars::JsonSchema>::_schemars_private_is_option()
+                        }
+                    };
                 };
-            };
-        });
+            });
+        }
     }
 
     // We don't know which contract is set on the schema generator here, so we
diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs
index c53db03..4f0d4c1 100644
--- a/schemars_derive/src/schema_exprs.rs
+++ b/schemars_derive/src/schema_exprs.rs
@@ -53,6 +53,9 @@ pub fn expr_for_container(cont: &Container) -> SchemaExpr {
         Data::Struct(Style::Unit, _) => expr_for_unit_struct(),
         Data::Struct(Style::Newtype, fields) => expr_for_newtype_struct(&fields[0]),
         Data::Struct(Style::Tuple, fields) => expr_for_tuple_struct(fields),
+        Data::Struct(Style::Struct, fields) if cont.serde_attrs.transparent() => {
+            expr_for_newtype_struct(&fields[0])
+        }
         Data::Struct(Style::Struct, fields) => expr_for_struct(
             fields,
             cont.serde_attrs.default(),


Test patch:
diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/transparent.rs~transparent_newtype_with_validation.json b/schemars/tests/integration/snapshots/schemars/tests/integration/transparent.rs~transparent_newtype_with_validation.json
new file mode 100644
index 0000000..2098bc9
--- /dev/null
+++ b/schemars/tests/integration/snapshots/schemars/tests/integration/transparent.rs~transparent_newtype_with_validation.json
@@ -0,0 +1,6 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "title": "TransparentNewTypeWithValidation",
+  "type": "string",
+  "minLength": 1
+}
\ No newline at end of file
diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/transparent.rs~transparent_struct_with_doc.json b/schemars/tests/integration/snapshots/schemars/tests/integration/transparent.rs~transparent_struct_with_doc.json
new file mode 100644
index 0000000..6bbb9be
--- /dev/null
+++ b/schemars/tests/integration/snapshots/schemars/tests/integration/transparent.rs~transparent_struct_with_doc.json
@@ -0,0 +1,6 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "title": "TransparentStructWithDoc",
+  "description": "A doc comment",
+  "type": "string"
+}
\ No newline at end of file
diff --git a/schemars/tests/integration/transparent.rs b/schemars/tests/integration/transparent.rs
index 2268cc5..53366ff 100644
--- a/schemars/tests/integration/transparent.rs
+++ b/schemars/tests/integration/transparent.rs
@@ -14,6 +14,21 @@ fn transparent_struct() {
         .assert_matches_de_roundtrip(arbitrary_values());
 }
 
+#[derive(JsonSchema, Deserialize, Serialize, Default)]
+#[serde(transparent)]
+/// A doc comment
+pub struct TransparentStructWithDoc {
+    inner: String,
+}
+
+#[test]
+fn transparent_struct_with_doc() {
+    test!(TransparentStructWithDoc)
+        .assert_snapshot()
+        .assert_allows_ser_roundtrip_default()
+        .assert_matches_de_roundtrip(arbitrary_values());
+}
+
 #[derive(JsonSchema, Deserialize, Serialize, Default)]
 #[serde(transparent)]
 pub struct TransparentNewType(String);
@@ -25,3 +40,16 @@ fn transparent_newtype() {
         .assert_allows_ser_roundtrip_default()
         .assert_matches_de_roundtrip(arbitrary_values());
 }
+
+#[derive(JsonSchema, Deserialize, Serialize)]
+#[serde(transparent)]
+pub struct TransparentNewTypeWithValidation(#[schemars(length(min = 1))] String);
+
+#[test]
+fn transparent_newtype_with_validation() {
+    test!(TransparentNewTypeWithValidation)
+        .with_validator(|v| !v.0.is_empty())
+        .assert_snapshot()
+        .assert_allows_ser_roundtrip([TransparentNewTypeWithValidation("a@a.com".into())])
+        .assert_matches_de_roundtrip(arbitrary_values());
+}