Repository: GREsau/schemars
Pull request #331 (2024-08-29 14:40:18)

Read `#[garde(...)]` attributes in addition to `#[validate(...)]`
Addresses #233

Add support for the following [garde](https://github.com/jprochazk/garde) attributes, along with allowing `#[schemars(...)]` overrides:
- `required`
- `email`
- `url`
- `ip`/`ipv4`/`ipv6`
- `length`
- `range`
- `contains`
- `pattern`
- `inner`

Does **not** support:
- `ascii`
- `alphanumeric`
- `prefix`/`suffix`
- `credit_card`
- `phone_number`
- `matches`
- `div`
- `skip`
- `custom`


Patch:
diff --git a/docs/0-migrating.md b/docs/0-migrating.md
index 4ee936a..499f32c 100644
--- a/docs/0-migrating.md
+++ b/docs/0-migrating.md
@@ -173,3 +173,23 @@ fn my_transform2(schema: &mut Schema) {
 let mut schema = schemars::schema_for!(str);
 RecursiveTransform(my_transform2).transform(&mut schema);
 ```
+
+## Changes to `#[validate(...)]` attributes
+
+Since [adding support for `#[validate(...)]` attributes](https://graham.cool/schemars/v0/deriving/attributes/#supported-validator-attributes), the [Validator](https://github.com/Keats/validator) crate has made several changes to its supported attributes. Accordingly, Schemars 1.0 has updated its handling of `#[validate(...)]` attributes to match the latest version (currently 0.18.1) of the Validator crate - this removes some attributes, and changes the syntax of others:
+
+- The `#[validate(phone)]`/`#[schemars(phone)]` attribute is removed. If you want the old behaviour of setting the "format" property on the generated schema, you can use `#[schemars(extend("format = "phone"))]` instead.
+- The `#[validate(required_nested)]`/`#[schemars(required_nested)]` attribute is removed. If you want the old behaviour, you can use `#[schemars(required)]` instead.
+- The `#[validate(regex = "...")]`/`#[schemars(regex = "...")]` attribute can no longer use `name = "value"` syntax. Instead, you can use:
+
+  - `#[validate(regex(path = ...)]`
+  - `#[schemars(regex(pattern = ...)]`
+  - `#[schemars(pattern(...)]` (Garde-style)
+
+- Similarly, the `#[validate(contains = "...")]`/`#[schemars(contains = "...")]` attribute can no longer use `name = "value"` syntax. Instead, you can use:
+
+  - `#[validate(contains(pattern = ...))]`
+  - `#[schemars(contains(pattern = ...))]`
+  - `#[schemars(contains(...))]` (Garde-style)
+
+As an alternative option, Schemars 1.0 also adds support for `#[garde(...)]` attributes used with the [Garde](https://github.com/jprochazk/garde) crate, along with equivalent `#[schemars(...)]` attributes. See [the documentation](https://graham.cool/schemars/deriving/attributes/#supported-validatorgarde-attributes) for a list of all supported attributes.
diff --git a/docs/1.1-attributes.md b/docs/1.1-attributes.md
index edb1dbd..a2159db 100644
--- a/docs/1.1-attributes.md
+++ b/docs/1.1-attributes.md
@@ -8,6 +8,7 @@ permalink: /deriving/attributes/
 <style>
 h3 code {
     font-weight: bold;
+    text-wrap: nowrap;
 }
 </style>
 
diff --git a/docs/_includes/attributes.md b/docs/_includes/attributes.md
index 3661634..5da084d 100644
--- a/docs/_includes/attributes.md
+++ b/docs/_includes/attributes.md
@@ -4,7 +4,7 @@ You can add attributes to your types to customize Schemars's derived `JsonSchema
 
 [Serde](https://serde.rs/) allows setting `#[serde(...)]` attributes which change how types are serialized, and Schemars will generally respect these attributes to ensure that generated schemas will match how the type is serialized by serde_json. `#[serde(...)]` attributes can be overriden using `#[schemars(...)]` attributes, which behave identically (e.g. `#[schemars(rename_all = "camelCase")]`). You may find this useful if you want to change the generated schema without affecting Serde's behaviour, or if you're just not using Serde.
 
-[Validator](https://github.com/Keats/validator) allows setting `#[validate(...)]` attributes to restrict valid values of particular fields, many of which will be used by Schemars to generate more accurate schemas. These can also be overridden by `#[schemars(...)]` attributes.
+[Validator](https://github.com/Keats/validator) and [Garde](https://github.com/jprochazk/garde) allow setting `#[validate(...)]`/`#[garde(...)]` attributes to restrict valid values of particular fields, many of which will be used by Schemars to generate more accurate schemas. These can also be overridden by `#[schemars(...)]` attributes.
 
 <details open>
 <summary style="font-weight: bold">
@@ -23,11 +23,11 @@ TABLE OF CONTENTS
    - [`flatten`](#flatten)
    - [`with`](#with)
    - [`bound`](#bound)
-1. [Supported Validator Attributes](#supported-validator-attributes)
-   - [`email` / `url`](#email-url)
+1. [Supported Validator/Garde Attributes](#supported-validatorgarde-attributes)
+   - [`email` / `url` / `ip` / `ipv4` / `ipv6`](#formats)
    - [`length`](#length)
    - [`range`](#range)
-   - [`regex`](#regex)
+   - [`regex` / `pattern`](#regex)
    - [`contains`](#contains)
    - [`required`](#required)
 1. [Other Attributes](#other-attributes)
@@ -184,25 +184,28 @@ Serde docs: [container](https://serde.rs/container-attrs.html#bound)
 
 </div>
 
-## Supported Validator Attributes
+## Supported Validator/Garde Attributes
 
 <div class="indented">
 
-<h3 id="email-url">
+<h3 id="formats">
 
-`#[validate(email)]` / `#[schemars(email)]`<br />
-`#[validate(url)]` / `#[schemars(url)]`
+`#[validate(email)]` / `#[garde(email)]` / `#[schemars(email)]`<br />
+`#[validate(url)]` / `#[garde(url)]`/ `#[schemars(url)]`<br />
+`#[garde(ip)]`/ `#[schemars(ip)]`<br />
+`#[garde(ipv4)]`/ `#[schemars(ipv4)]`<br />
+`#[garde(ipv6)]`/ `#[schemars(ip)v6]`<br />
 
 </h3>
 
-Sets the schema's `format` to `email`/`uri`, as appropriate. Only one of these attributes may be present on a single field.
+Sets the schema's `format` to `email`/`uri`/`ip`/`ipv4`/`ipv6`, as appropriate. Only one of these attributes may be present on a single field.
 
 Validator docs: [email](https://github.com/Keats/validator#email) / [url](https://github.com/Keats/validator#url)
 
 <h3 id="length">
 
-`#[validate(length(min = 1, max = 10))]` / `#[schemars(length(min = 1, max = 10))]`<br />
-`#[validate(length(equal = 10))]` / `#[schemars(length(equal = 10))]`
+`#[validate(length(min = 1, max = 10))]` / `#[garde(length(min = 1, max = 10))]` / `#[schemars(length(min = 1, max = 10))]`<br />
+`#[validate(length(equal = 10))]` / `#[garde(length(equal = 10))]` / `#[schemars(length(equal = 10))]`
 
 </h3>
 
@@ -212,7 +215,7 @@ Validator docs: [length](https://github.com/Keats/validator#length)
 
 <h3 id="range">
 
-`#[validate(range(min = 1, max = 10))]` / `#[schemars(range(min = 1, max = 10))]`
+`#[validate(range(min = 1, max = 10))]` / `#[garde(range(min = 1, max = 10))]` / `#[schemars(range(min = 1, max = 10))]`
 
 </h3>
 
@@ -223,29 +226,31 @@ Validator docs: [range](https://github.com/Keats/validator#range)
 <h3 id="regex">
 
 `#[validate(regex(path = *static_regex)]`<br />
-`#[schemars(regex(pattern = r"^\d+$"))]` / `#[schemars(regex(pattern = *static_regex))]`
+`#[schemars(regex(pattern = r"^\d+$"))]` / `#[schemars(regex(pattern = *static_regex))]`<br />
+`#[garde(pattern(r"^\d+$")]` / `#[schemars(pattern(r"^\d+$")]`/ `#[schemars(pattern(*static_regex)]`
 
 </h3>
 
 Sets the `pattern` property for string schemas. The `static_regex` will typically refer to a [`Regex`](https://docs.rs/regex/*/regex/struct.Regex.html) instance, but Schemars allows it to be any value with a `to_string()` method.
 
-`regex(pattern = ...)` is a Schemars extension, and not currently supported by the Validator crate. When using this form, you may want to use a `r"raw string literal"` so that `\\` characters in the regex pattern are not interpreted as escape sequences in the string. Using the `path` form is not allowed in a `#[schemars(...)]` attribute.
+`regex(pattern = ...)` is a Schemars extension, and not currently supported by the Validator crate. When using this form (or the Garde-style `pattern` attribute), you may want to use a `r"raw string literal"` so that `\\` characters in the regex pattern are not interpreted as escape sequences in the string. Using the `path` form is not allowed in a `#[schemars(...)]` attribute.
 
 Validator docs: [regex](https://github.com/Keats/validator#regex)
 
 <h3 id="contains">
 
-`#[validate(contains(pattern = "string"))]` / `#[schemars(contains(pattern = "string"))]`
+`#[validate(contains(pattern = "string"))]` / `#[schemars(contains(pattern = "string"))]`<br />
+`#[garde(contains("string"))]` / `#[schemars(contains("string"))]`
 
 </h3>
 
-For string schemas, sets the `pattern` property to the given value, with any regex special characters escaped. For object schemas (e.g. when the attribute is set on a HashMap field), includes the value in the `required` property, indicating that the map must contain it as a key.
+For string schemas, sets the `pattern` property to the given value, with any regex special characters escaped.
 
 Validator docs: [contains](https://github.com/Keats/validator#contains)
 
 <h3 id="required">
 
-`#[validate(required)]` / `#[schemars(required)]`<br />
+`#[validate(required)]` / `#[garde(required)]` / `#[schemars(required)]`<br />
 
 </h3>
 
@@ -305,7 +310,7 @@ Set the path to the schemars crate instance the generated code should depend on.
 
 </h3>
 
-Sets properties specified by [validator attributes](#supported-validator-attributes) on items of an array schema. For example:
+Sets properties specified by [validator attributes](#supported-validatorgarde-attributes) on items of an array schema. For example:
 
 ```rust
 struct Struct {
diff --git a/schemars/src/_private/mod.rs b/schemars/src/_private/mod.rs
index 75966b8..404d015 100644
--- a/schemars/src/_private/mod.rs
+++ b/schemars/src/_private/mod.rs
@@ -199,26 +199,9 @@ pub fn insert_validation_property(
     }
 }
 
-pub fn must_contain(schema: &mut Schema, contain: String) {
-    if schema.has_type("string") {
-        let pattern = regex_syntax::escape(&contain);
-        schema
-            .ensure_object()
-            .insert("pattern".to_owned(), pattern.into());
-    }
-
-    if schema.has_type("object") {
-        if let Value::Array(array) = schema
-            .ensure_object()
-            .entry("required")
-            .or_insert(Value::Array(Vec::new()))
-        {
-            let value = Value::from(contain);
-            if !array.contains(&value) {
-                array.push(value);
-            }
-        }
-    }
+pub fn must_contain(schema: &mut Schema, substring: &str) {
+    let escaped = regex_syntax::escape(substring);
+    insert_validation_property(schema, "string", "pattern", escaped);
 }
 
 pub fn apply_inner_validation(schema: &mut Schema, f: fn(&mut Schema) -> ()) {
diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs
index 9698731..28bc192 100644
--- a/schemars_derive/src/attr/mod.rs
+++ b/schemars_derive/src/attr/mod.rs
@@ -210,9 +210,10 @@ impl FieldAttrs {
         let schemars_cx = &mut AttrCtxt::new(cx, attrs, "schemars");
         let serde_cx = &mut AttrCtxt::new(cx, attrs, "serde");
         let validate_cx = &mut AttrCtxt::new(cx, attrs, "validate");
+        let garde_cx = &mut AttrCtxt::new(cx, attrs, "garde");
 
         self.common.populate(attrs, schemars_cx, serde_cx);
-        self.validation.populate(schemars_cx, validate_cx);
+        self.validation.populate(schemars_cx, validate_cx, garde_cx);
         self.process_attr(schemars_cx);
         self.process_attr(serde_cx);
     }
@@ -277,6 +278,7 @@ impl ContainerAttrs {
                 None => self.crate_name = parse_name_value_lit_str(meta, cx).ok(),
             },
 
+            // The actual parsing of `rename` is done by serde
             "rename" => self.is_renamed = true,
 
             _ => return Some(meta),
diff --git a/schemars_derive/src/attr/parse_meta.rs b/schemars_derive/src/attr/parse_meta.rs
index 9259641..94c89ee 100644
--- a/schemars_derive/src/attr/parse_meta.rs
+++ b/schemars_derive/src/attr/parse_meta.rs
@@ -103,7 +103,7 @@ pub fn parse_extensions(
     cx: &AttrCtxt,
 ) -> Result<impl IntoIterator<Item = Extension>, ()> {
     let parser = Punctuated::<Extension, Token![,]>::parse_terminated;
-    parse_meta_list(meta, cx, parser)
+    parse_meta_list_with(&meta, cx, parser)
 }
 
 pub fn parse_length_or_range(outer_meta: Meta, cx: &AttrCtxt) -> Result<LengthOrRange, ()> {
@@ -144,6 +144,10 @@ pub fn parse_length_or_range(outer_meta: Meta, cx: &AttrCtxt) -> Result<LengthOr
     Ok(result)
 }
 
+pub fn parse_pattern(meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
+    parse_meta_list_with(&meta, cx, Expr::parse)
+}
+
 pub fn parse_schemars_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
     let mut pattern = None;
 
@@ -200,9 +204,47 @@ pub fn parse_validate_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()>
 }
 
 pub fn parse_contains(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
+    #[derive(Debug)]
+    enum ContainsFormat {
+        Metas(Punctuated<Meta, Token![,]>),
+        Expr(Expr),
+    }
+
+    impl Parse for ContainsFormat {
+        fn parse(input: ParseStream) -> syn::Result<Self> {
+            // An imperfect but good-enough heuristic for determining whether it looks more like a
+            // comma-separated meta list (validator-style), or a single expression (garde-style).
+            // This heuristic may not generalise well-enough for attributes other than `contains`!
+            // `foo = bar` => Metas (not Expr::Assign)
+            // `foo, bar`  => Metas
+            // `foo`       => Expr (not Meta::Path)
+            // `foo(bar)`  => Expr (not Meta::List)
+            if input.peek2(Token![,]) || input.peek2(Token![=]) {
+                Punctuated::parse_terminated(input).map(Self::Metas)
+            } else {
+                input.parse().map(Self::Expr)
+            }
+        }
+    }
+
+    let nested_meta_or_expr = match cx.attr_type {
+        "validate" => parse_meta_list_with(&outer_meta, cx, Punctuated::parse_terminated)
+            .map(ContainsFormat::Metas),
+        "garde" => parse_meta_list_with(&outer_meta, cx, Expr::parse).map(ContainsFormat::Expr),
+        "schemars" => parse_meta_list_with(&outer_meta, cx, ContainsFormat::parse),
+        wat => {
+            unreachable!("Unexpected attr type `{wat}` for `contains` item. This is a bug in schemars, please raise an issue!")
+        }
+    }?;
+
+    let nested_metas = match nested_meta_or_expr {
+        ContainsFormat::Expr(expr) => return Ok(expr),
+        ContainsFormat::Metas(m) => m,
+    };
+
     let mut pattern = None;
 
-    for nested_meta in parse_nested_meta(outer_meta.clone(), cx)? {
+    for nested_meta in nested_metas {
         match path_str(nested_meta.path()).as_str() {
             "pattern" => match &pattern {
                 Some(_) => cx.duplicate_error(&nested_meta),
@@ -229,10 +271,10 @@ pub fn parse_contains(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
 
 pub fn parse_nested_meta(meta: Meta, cx: &AttrCtxt) -> Result<impl IntoIterator<Item = Meta>, ()> {
     let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
-    parse_meta_list(meta, cx, parser)
+    parse_meta_list_with(&meta, cx, parser)
 }
 
-fn parse_meta_list<F: Parser>(meta: Meta, cx: &AttrCtxt, parser: F) -> Result<F::Output, ()> {
+fn parse_meta_list_with<F: Parser>(meta: &Meta, cx: &AttrCtxt, parser: F) -> Result<F::Output, ()> {
     let Meta::List(meta_list) = meta else {
         let name = path_str(meta.path());
         cx.error_spanned_by(
diff --git a/schemars_derive/src/attr/validation.rs b/schemars_derive/src/attr/validation.rs
index 41031db..c8bf880 100644
--- a/schemars_derive/src/attr/validation.rs
+++ b/schemars_derive/src/attr/validation.rs
@@ -5,8 +5,8 @@ use crate::idents::SCHEMA;
 
 use super::{
     parse_meta::{
-        parse_contains, parse_length_or_range, parse_nested_meta, parse_schemars_regex,
-        parse_validate_regex, require_path_only, LengthOrRange,
+        parse_contains, parse_length_or_range, parse_nested_meta, parse_pattern,
+        parse_schemars_regex, parse_validate_regex, require_path_only, LengthOrRange,
     },
     AttrCtxt,
 };
@@ -15,6 +15,9 @@ use super::{
 pub enum Format {
     Email,
     Uri,
+    Ip,
+    Ipv4,
+    Ipv6,
 }
 
 impl Format {
@@ -22,6 +25,9 @@ impl Format {
         match self {
             Format::Email => "email",
             Format::Uri => "url",
+            Format::Ip => "ip",
+            Format::Ipv4 => "ipv4",
+            Format::Ipv6 => "ipv6",
         }
     }
 
@@ -29,6 +35,9 @@ impl Format {
         match self {
             Format::Email => "email",
             Format::Uri => "uri",
+            Format::Ip => "ip",
+            Format::Ipv4 => "ipv4",
+            Format::Ipv6 => "ipv6",
         }
     }
 
@@ -36,6 +45,9 @@ impl Format {
         Some(match s {
             "email" => Format::Email,
             "url" => Format::Uri,
+            "ip" => Format::Ip,
+            "ipv4" => Format::Ipv4,
+            "ipv6" => Format::Ipv6,
             _ => return None,
         })
     }
@@ -45,6 +57,7 @@ impl Format {
 pub struct ValidationAttrs {
     pub length: Option<LengthOrRange>,
     pub range: Option<LengthOrRange>,
+    pub pattern: Option<Expr>,
     pub regex: Option<Expr>,
     pub contains: Option<Expr>,
     pub required: bool,
@@ -67,7 +80,7 @@ impl ValidationAttrs {
             Self::add_length_or_range(range, mutators, "number", "imum", mut_ref_schema);
         }
 
-        if let Some(regex) = &self.regex {
+        if let Some(regex) = self.regex.as_ref().or(self.pattern.as_ref()) {
             mutators.push(quote! {
                 schemars::_private::insert_validation_property(#mut_ref_schema, "string", "pattern", (#regex).to_string());
             });
@@ -75,7 +88,7 @@ impl ValidationAttrs {
 
         if let Some(contains) = &self.contains {
             mutators.push(quote! {
-                schemars::_private::must_contain(#mut_ref_schema, #contains.to_string());
+                schemars::_private::must_contain(#mut_ref_schema, &#contains.to_string());
             });
         }
 
@@ -120,9 +133,15 @@ impl ValidationAttrs {
         }
     }
 
-    pub(super) fn populate(&mut self, schemars_cx: &mut AttrCtxt, validate_cx: &mut AttrCtxt) {
+    pub(super) fn populate(
+        &mut self,
+        schemars_cx: &mut AttrCtxt,
+        validate_cx: &mut AttrCtxt,
+        garde_cx: &mut AttrCtxt,
+    ) {
         self.process_attr(schemars_cx);
         self.process_attr(validate_cx);
+        self.process_attr(garde_cx);
     }
 
     fn process_attr(&mut self, cx: &mut AttrCtxt) {
@@ -153,22 +172,36 @@ impl ValidationAttrs {
                 }
             }
 
-            "regex" => match (&self.regex, &self.contains, cx.attr_type) {
-                (Some(_), _, _) => cx.duplicate_error(&meta),
-                (_, Some(_), _) => cx.mutual_exclusive_error(&meta, "contains"),
-                (None, None, "schemars") => self.regex = parse_schemars_regex(meta, cx).ok(),
-                (None, None, "validate") => self.regex = parse_validate_regex(meta, cx).ok(),
-                (None, None, wat) => {
-                    unreachable!("Unexpected attr type `{wat}` for regex item. This is a bug in schemars, please raise an issue!")
+            "pattern" if cx.attr_type != "validate" => {
+                match (&self.pattern, &self.regex, &self.contains) {
+                    (Some(_p), _, _) => cx.duplicate_error(&meta),
+                    (_, Some(_r), _) => cx.mutual_exclusive_error(&meta, "regex"),
+                    (_, _, Some(_c)) => cx.mutual_exclusive_error(&meta, "contains"),
+                    (None, None, None) => self.pattern = parse_pattern(meta, cx).ok(),
                 }
-            },
-            "contains" => match (&self.regex, &self.contains) {
-                (Some(_), _) => cx.mutual_exclusive_error(&meta, "regex"),
-                (_, Some(_)) => cx.duplicate_error(&meta),
-                (None, None) => self.contains = parse_contains(meta, cx).ok(),
+            }
+            "regex" if cx.attr_type != "garde" => {
+                match (&self.pattern, &self.regex, &self.contains) {
+                    (Some(_p), _, _) => cx.mutual_exclusive_error(&meta, "pattern"),
+                    (_, Some(_r), _) => cx.duplicate_error(&meta),
+                    (_, _, Some(_c)) => cx.mutual_exclusive_error(&meta, "contains"),
+                    (None, None, None) => {
+                        if cx.attr_type == "validate" {
+                            self.regex = parse_validate_regex(meta, cx).ok()
+                        } else {
+                            self.regex = parse_schemars_regex(meta, cx).ok()
+                        }
+                    }
+                }
+            }
+            "contains" => match (&self.pattern, &self.regex, &self.contains) {
+                (Some(_p), _, _) => cx.mutual_exclusive_error(&meta, "pattern"),
+                (_, Some(_r), _) => cx.mutual_exclusive_error(&meta, "regex"),
+                (_, _, Some(_c)) => cx.duplicate_error(&meta),
+                (None, None, None) => self.contains = parse_contains(meta, cx).ok(),
             },
 
-            "inner" => {
+            "inner" if cx.attr_type != "validate" => {
                 if let Ok(nested_meta) = parse_nested_meta(meta, cx) {
                     let inner = self
                         .inner
diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs
index 48afe80..578f45c 100644
--- a/schemars_derive/src/lib.rs
+++ b/schemars_derive/src/lib.rs
@@ -18,7 +18,7 @@ use syn::spanned::Spanned;
 
 #[doc = "Derive macro for `JsonSchema` trait."]
 #[cfg_attr(not(doctest), doc = include_str!("../deriving.md"), doc = include_str!("../attributes.md"))]
-#[proc_macro_derive(JsonSchema, attributes(schemars, serde, validate))]
+#[proc_macro_derive(JsonSchema, attributes(schemars, serde, validate, garde))]
 pub fn derive_json_schema_wrapper(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let input = parse_macro_input!(input as syn::DeriveInput);
     derive_json_schema(input, false)


Test patch:
diff --git a/schemars/tests/expected/garde.json b/schemars/tests/expected/garde.json
new file mode 100644
index 0000000..1f5d8a6
--- /dev/null
+++ b/schemars/tests/expected/garde.json
@@ -0,0 +1,74 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "title": "Struct",
+  "type": "object",
+  "properties": {
+    "min_max": {
+      "type": "number",
+      "format": "float",
+      "minimum": 0.01,
+      "maximum": 100
+    },
+    "min_max2": {
+      "type": "number",
+      "format": "float",
+      "minimum": 1,
+      "maximum": 1000
+    },
+    "regex_str1": {
+      "type": "string",
+      "pattern": "^[Hh]ello\\b"
+    },
+    "contains_str1": {
+      "type": "string",
+      "pattern": "substring\\.\\.\\."
+    },
+    "email_address": {
+      "type": "string",
+      "format": "email"
+    },
+    "homepage": {
+      "type": "string",
+      "format": "uri"
+    },
+    "non_empty_str": {
+      "type": "string",
+      "minLength": 1,
+      "maxLength": 100
+    },
+    "non_empty_str2": {
+      "type": "string",
+      "minLength": 1,
+      "maxLength": 1000
+    },
+    "pair": {
+      "type": "array",
+      "items": {
+        "type": "integer",
+        "format": "int32"
+      },
+      "minItems": 2,
+      "maxItems": 2
+    },
+    "required_option": {
+      "type": "boolean"
+    },
+    "x": {
+      "type": "integer",
+      "format": "int32"
+    }
+  },
+  "required": [
+    "min_max",
+    "min_max2",
+    "regex_str1",
+    "contains_str1",
+    "email_address",
+    "homepage",
+    "non_empty_str",
+    "non_empty_str2",
+    "pair",
+    "required_option",
+    "x"
+  ]
+}
\ No newline at end of file
diff --git a/schemars/tests/expected/garde_newtype.json b/schemars/tests/expected/garde_newtype.json
new file mode 100644
index 0000000..cd835f5
--- /dev/null
+++ b/schemars/tests/expected/garde_newtype.json
@@ -0,0 +1,8 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "title": "NewType",
+  "type": "integer",
+  "format": "uint8",
+  "minimum": 0,
+  "maximum": 10
+}
\ No newline at end of file
diff --git a/schemars/tests/expected/garde_schemars_attrs.json b/schemars/tests/expected/garde_schemars_attrs.json
new file mode 100644
index 0000000..f548706
--- /dev/null
+++ b/schemars/tests/expected/garde_schemars_attrs.json
@@ -0,0 +1,74 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "title": "Struct2",
+  "type": "object",
+  "properties": {
+    "min_max": {
+      "type": "number",
+      "format": "float",
+      "minimum": 0.01,
+      "maximum": 100
+    },
+    "min_max2": {
+      "type": "number",
+      "format": "float",
+      "minimum": 1,
+      "maximum": 1000
+    },
+    "regex_str1": {
+      "type": "string",
+      "pattern": "^[Hh]ello\\b"
+    },
+    "contains_str1": {
+      "type": "string",
+      "pattern": "substring\\.\\.\\."
+    },
+    "email_address": {
+      "type": "string",
+      "format": "email"
+    },
+    "homepage": {
+      "type": "string",
+      "format": "uri"
+    },
+    "non_empty_str": {
+      "type": "string",
+      "minLength": 1,
+      "maxLength": 100
+    },
+    "non_empty_str2": {
+      "type": "string",
+      "minLength": 1,
+      "maxLength": 1000
+    },
+    "pair": {
+      "type": "array",
+      "items": {
+        "type": "integer",
+        "format": "int32"
+      },
+      "minItems": 2,
+      "maxItems": 2
+    },
+    "required_option": {
+      "type": "boolean"
+    },
+    "x": {
+      "type": "integer",
+      "format": "int32"
+    }
+  },
+  "required": [
+    "min_max",
+    "min_max2",
+    "regex_str1",
+    "contains_str1",
+    "email_address",
+    "homepage",
+    "non_empty_str",
+    "non_empty_str2",
+    "pair",
+    "required_option",
+    "x"
+  ]
+}
\ No newline at end of file
diff --git a/schemars/tests/expected/garde_tuple.json b/schemars/tests/expected/garde_tuple.json
new file mode 100644
index 0000000..fa81224
--- /dev/null
+++ b/schemars/tests/expected/garde_tuple.json
@@ -0,0 +1,18 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "title": "Tuple",
+  "type": "array",
+  "prefixItems": [
+    {
+      "type": "integer",
+      "format": "uint8",
+      "minimum": 0,
+      "maximum": 10
+    },
+    {
+      "type": "boolean"
+    }
+  ],
+  "minItems": 2,
+  "maxItems": 2
+}
\ No newline at end of file
diff --git a/schemars/tests/expected/validate.json b/schemars/tests/expected/validate.json
index b2c893a..64b571f 100644
--- a/schemars/tests/expected/validate.json
+++ b/schemars/tests/expected/validate.json
@@ -58,15 +58,6 @@
       "minItems": 2,
       "maxItems": 2
     },
-    "map_contains": {
-      "type": "object",
-      "additionalProperties": {
-        "type": "null"
-      },
-      "required": [
-        "map_key"
-      ]
-    },
     "required_option": {
       "type": "boolean"
     },
@@ -87,7 +78,6 @@
     "non_empty_str",
     "non_empty_str2",
     "pair",
-    "map_contains",
     "required_option",
     "x"
   ]
diff --git a/schemars/tests/expected/validate_schemars_attrs.json b/schemars/tests/expected/validate_schemars_attrs.json
index e258f18..1f2e3eb 100644
--- a/schemars/tests/expected/validate_schemars_attrs.json
+++ b/schemars/tests/expected/validate_schemars_attrs.json
@@ -58,15 +58,6 @@
       "minItems": 2,
       "maxItems": 2
     },
-    "map_contains": {
-      "type": "object",
-      "additionalProperties": {
-        "type": "null"
-      },
-      "required": [
-        "map_key"
-      ]
-    },
     "required_option": {
       "type": "boolean"
     },
@@ -87,7 +78,6 @@
     "non_empty_str",
     "non_empty_str2",
     "pair",
-    "map_contains",
     "required_option",
     "x"
   ]
diff --git a/schemars/tests/garde.rs b/schemars/tests/garde.rs
new file mode 100644
index 0000000..8b7ce85
--- /dev/null
+++ b/schemars/tests/garde.rs
@@ -0,0 +1,99 @@
+mod util;
+use schemars::JsonSchema;
+use util::*;
+
+const MIN: u32 = 1;
+const MAX: u32 = 1000;
+
+#[allow(dead_code)]
+#[derive(JsonSchema)]
+pub struct Struct {
+    #[garde(range(min = 0.01, max = 100))]
+    min_max: f32,
+    #[garde(range(min = MIN, max = MAX))]
+    min_max2: f32,
+    #[garde(pattern(r"^[Hh]ello\b"))]
+    regex_str1: String,
+    #[garde(contains(concat!("sub","string...")))]
+    contains_str1: String,
+    #[garde(email)]
+    email_address: String,
+    #[garde(url)]
+    homepage: String,
+    #[garde(length(min = 1, max = 100))]
+    non_empty_str: String,
+    #[garde(length(min = MIN, max = MAX))]
+    non_empty_str2: String,
+    #[garde(length(equal = 2))]
+    pair: Vec<i32>,
+    #[garde(required)]
+    required_option: Option<bool>,
+    #[garde(required)]
+    #[serde(flatten)]
+    required_flattened: Option<Inner>,
+}
+
+#[allow(dead_code)]
+#[derive(JsonSchema)]
+pub struct Inner {
+    x: i32,
+}
+
+#[test]
+fn garde() -> TestResult {
+    test_default_generated_schema::<Struct>("garde")
+}
+
+#[allow(dead_code)]
+#[derive(JsonSchema)]
+pub struct Struct2 {
+    #[schemars(range(min = 0.01, max = 100))]
+    min_max: f32,
+    #[schemars(range(min = MIN, max = MAX))]
+    min_max2: f32,
+    #[schemars(pattern(r"^[Hh]ello\b"))]
+    regex_str1: String,
+    #[schemars(contains(concat!("sub","string...")))]
+    contains_str1: String,
+    #[schemars(email)]
+    email_address: String,
+    #[schemars(url)]
+    homepage: String,
+    #[schemars(length(min = 1, max = 100))]
+    non_empty_str: String,
+    #[schemars(length(min = MIN, max = MAX))]
+    non_empty_str2: String,
+    #[schemars(length(equal = 2))]
+    pair: Vec<i32>,
+    #[schemars(required)]
+    required_option: Option<bool>,
+    #[schemars(required)]
+    #[serde(flatten)]
+    required_flattened: Option<Inner>,
+}
+
+#[test]
+fn garde_schemars_attrs() -> TestResult {
+    test_default_generated_schema::<Struct2>("garde_schemars_attrs")
+}
+
+#[allow(dead_code)]
+#[derive(JsonSchema)]
+pub struct Tuple(
+    #[garde(range(max = 10))] u8,
+    #[garde(required)] Option<bool>,
+);
+
+#[test]
+fn garde_tuple() -> TestResult {
+    test_default_generated_schema::<Tuple>("garde_tuple")
+}
+
+#[allow(dead_code)]
+#[derive(JsonSchema)]
+pub struct NewType(#[garde(range(max = 10))] u8);
+
+#[test]
+fn garde_newtype() -> TestResult {
+    test_default_generated_schema::<NewType>("garde_newtype")
+}
diff --git a/schemars/tests/validate.rs b/schemars/tests/validate.rs
index bd4f3a1..410593d 100644
--- a/schemars/tests/validate.rs
+++ b/schemars/tests/validate.rs
@@ -1,6 +1,5 @@
 mod util;
 use schemars::JsonSchema;
-use std::collections::BTreeMap;
 use util::*;
 
 struct FakeRegex(&'static str);
@@ -42,8 +41,6 @@ pub struct Struct {
     non_empty_str2: String,
     #[validate(length(equal = 2))]
     pair: Vec<i32>,
-    #[validate(contains(pattern = "map_key"))]
-    map_contains: BTreeMap<String, ()>,
     #[validate(required)]
     required_option: Option<bool>,
     #[validate(required)]
@@ -90,8 +87,6 @@ pub struct Struct2 {
     non_empty_str2: String,
     #[schemars(length(equal = 2))]
     pair: Vec<i32>,
-    #[schemars(contains(pattern = "map_key"))]
-    map_contains: BTreeMap<String, ()>,
     #[schemars(required)]
     required_option: Option<bool>,
     #[schemars(required)]