diff --git a/docs/1.1-attributes.md b/docs/1.1-attributes.md
index 251a6a10..2ce0b65a 100644
--- a/docs/1.1-attributes.md
+++ b/docs/1.1-attributes.md
@@ -276,6 +276,20 @@ Set the Rust built-in [`deprecated`](https://doc.rust-lang.org/edition-guide/rus
Set the path to the schemars crate instance the generated code should depend on. This is mostly useful for other crates that depend on schemars in their macros.
+
+
+`#[schemars(inner(...))]`
+
+
+Sets properties specified by [validator attributes](#supported-validator-attributes) on items of an array schema. For example:
+
+```rs
+struct Struct {
+ #[schemars(inner(url, regex(pattern = "^https://")))]
+ urls: Vec,
+}
+```
+
Doc Comments (`#[doc = "..."]`)
diff --git a/docs/_includes/examples/schemars_attrs.rs b/docs/_includes/examples/schemars_attrs.rs
index cd69b527..4ad2503f 100644
--- a/docs/_includes/examples/schemars_attrs.rs
+++ b/docs/_includes/examples/schemars_attrs.rs
@@ -10,6 +10,8 @@ pub struct MyStruct {
pub my_bool: bool,
#[schemars(default)]
pub my_nullable_enum: Option,
+ #[schemars(inner(regex(pattern = "^x$")))]
+ pub my_vec_str: Vec,
}
#[derive(Deserialize, Serialize, JsonSchema)]
diff --git a/docs/_includes/examples/schemars_attrs.schema.json b/docs/_includes/examples/schemars_attrs.schema.json
index 958cb6bb..9a6a22a6 100644
--- a/docs/_includes/examples/schemars_attrs.schema.json
+++ b/docs/_includes/examples/schemars_attrs.schema.json
@@ -4,7 +4,8 @@
"type": "object",
"required": [
"myBool",
- "myNumber"
+ "myNumber",
+ "myVecStr"
],
"properties": {
"myBool": {
@@ -26,6 +27,13 @@
"format": "int32",
"maximum": 10.0,
"minimum": 1.0
+ },
+ "myVecStr": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^x$"
+ }
}
},
"additionalProperties": false,
diff --git a/schemars/examples/schemars_attrs.rs b/schemars/examples/schemars_attrs.rs
index cd69b527..4ad2503f 100644
--- a/schemars/examples/schemars_attrs.rs
+++ b/schemars/examples/schemars_attrs.rs
@@ -10,6 +10,8 @@ pub struct MyStruct {
pub my_bool: bool,
#[schemars(default)]
pub my_nullable_enum: Option,
+ #[schemars(inner(regex(pattern = "^x$")))]
+ pub my_vec_str: Vec,
}
#[derive(Deserialize, Serialize, JsonSchema)]
diff --git a/schemars/examples/schemars_attrs.schema.json b/schemars/examples/schemars_attrs.schema.json
index 958cb6bb..9a6a22a6 100644
--- a/schemars/examples/schemars_attrs.schema.json
+++ b/schemars/examples/schemars_attrs.schema.json
@@ -4,7 +4,8 @@
"type": "object",
"required": [
"myBool",
- "myNumber"
+ "myNumber",
+ "myVecStr"
],
"properties": {
"myBool": {
@@ -26,6 +27,13 @@
"format": "int32",
"maximum": 10.0,
"minimum": 1.0
+ },
+ "myVecStr": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^x$"
+ }
}
},
"additionalProperties": false,
diff --git a/schemars/tests/expected/validate_inner.json b/schemars/tests/expected/validate_inner.json
new file mode 100644
index 00000000..443a9fa6
--- /dev/null
+++ b/schemars/tests/expected/validate_inner.json
@@ -0,0 +1,74 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Struct",
+ "type": "object",
+ "required": [
+ "array_str_length",
+ "slice_str_contains",
+ "vec_i32_range",
+ "vec_str_length",
+ "vec_str_length2",
+ "vec_str_regex",
+ "vec_str_url"
+ ],
+ "properties": {
+ "array_str_length": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "maxLength": 100,
+ "minLength": 5
+ },
+ "maxItems": 2,
+ "minItems": 2
+ },
+ "slice_str_contains": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "substring\\.\\.\\."
+ }
+ },
+ "vec_i32_range": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "format": "int32",
+ "maximum": 10.0,
+ "minimum": -10.0
+ }
+ },
+ "vec_str_length": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "maxLength": 100,
+ "minLength": 1
+ }
+ },
+ "vec_str_length2": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "maxLength": 100,
+ "minLength": 1
+ },
+ "maxItems": 3,
+ "minItems": 1
+ },
+ "vec_str_regex": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^[Hh]ello\\b"
+ }
+ },
+ "vec_str_url": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uri"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/schemars/tests/expected/validate_schemars_attrs.json b/schemars/tests/expected/validate_schemars_attrs.json
index d4a14e3f..86658ac4 100644
--- a/schemars/tests/expected/validate_schemars_attrs.json
+++ b/schemars/tests/expected/validate_schemars_attrs.json
@@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
- "title": "Struct",
+ "title": "Struct2",
"type": "object",
"required": [
"contains_str1",
diff --git a/schemars/tests/validate.rs b/schemars/tests/validate.rs
index e3817343..bf28d62c 100644
--- a/schemars/tests/validate.rs
+++ b/schemars/tests/validate.rs
@@ -101,7 +101,7 @@ pub struct Struct2 {
#[test]
fn validate_schemars_attrs() -> TestResult {
- test_default_generated_schema::("validate_schemars_attrs")
+ test_default_generated_schema::("validate_schemars_attrs")
}
#[derive(JsonSchema)]
diff --git a/schemars/tests/validate_inner.rs b/schemars/tests/validate_inner.rs
new file mode 100644
index 00000000..535410f1
--- /dev/null
+++ b/schemars/tests/validate_inner.rs
@@ -0,0 +1,31 @@
+mod util;
+
+use schemars::JsonSchema;
+use util::*;
+
+// In real code, this would typically be a Regex, potentially created in a `lazy_static!`.
+static STARTS_WITH_HELLO: &str = r"^[Hh]ello\b";
+
+#[allow(dead_code)]
+#[derive(JsonSchema)]
+pub struct Struct<'a> {
+ #[schemars(inner(length(min = 5, max = 100)))]
+ array_str_length: [&'a str; 2],
+ #[schemars(inner(contains(pattern = "substring...")))]
+ slice_str_contains: &'a[&'a str],
+ #[schemars(inner(regex = "STARTS_WITH_HELLO"))]
+ vec_str_regex: Vec,
+ #[schemars(inner(length(min = 1, max = 100)))]
+ vec_str_length: Vec<&'a str>,
+ #[schemars(length(min = 1, max = 3), inner(length(min = 1, max = 100)))]
+ vec_str_length2: Vec,
+ #[schemars(inner(url))]
+ vec_str_url: Vec,
+ #[schemars(inner(range(min = -10, max = 10)))]
+ vec_i32_range: Vec,
+}
+
+#[test]
+fn validate_inner() -> TestResult {
+ test_default_generated_schema::("validate_inner")
+}
diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs
index 79b468a2..f95bc485 100644
--- a/schemars_derive/src/attr/mod.rs
+++ b/schemars_derive/src/attr/mod.rs
@@ -103,11 +103,7 @@ impl Attrs {
}
};
- for meta_item in attrs
- .iter()
- .flat_map(|attr| get_meta_items(attr, attr_type, errors, ignore_errors))
- .flatten()
- {
+ for meta_item in get_meta_items(attrs, attr_type, errors, ignore_errors) {
match &meta_item {
Meta(NameValue(m)) if m.path.is_ident("with") => {
if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.lit) {
@@ -167,6 +163,12 @@ impl Attrs {
_ if ignore_errors => {}
+ Meta(List(m)) if m.path.is_ident("inner") && attr_type == "schemars" => {
+ // This will be processed with the validation attributes.
+ // It's allowed only for the schemars attribute because the
+ // validator crate doesn't support it yet.
+ }
+
Meta(meta_item) => {
if !is_known_serde_or_validation_keyword(meta_item) {
let path = meta_item
@@ -214,30 +216,25 @@ fn is_known_serde_or_validation_keyword(meta: &syn::Meta) -> bool {
}
fn get_meta_items(
- attr: &syn::Attribute,
+ attrs: &[syn::Attribute],
attr_type: &'static str,
errors: &Ctxt,
ignore_errors: bool,
-) -> Result, ()> {
- if !attr.path.is_ident(attr_type) {
- return Ok(Vec::new());
- }
-
- match attr.parse_meta() {
- Ok(List(meta)) => Ok(meta.nested.into_iter().collect()),
- Ok(other) => {
- if !ignore_errors {
- errors.error_spanned_by(other, format!("expected #[{}(...)]", attr_type))
- }
- Err(())
+) -> Vec {
+ attrs.iter().fold(vec![], |mut acc, attr| {
+ if !attr.path.is_ident(attr_type) {
+ return acc;
}
- Err(err) => {
- if !ignore_errors {
- errors.error_spanned_by(attr, err)
+ match attr.parse_meta() {
+ Ok(List(meta)) => acc.extend(meta.nested),
+ Ok(other) if !ignore_errors => {
+ errors.error_spanned_by(other, format!("expected #[{}(...)]", attr_type))
}
- Err(())
+ Err(err) if !ignore_errors => errors.error_spanned_by(attr, err),
+ _ => (),
}
- }
+ acc
+ })
}
fn get_lit_str<'a>(
diff --git a/schemars_derive/src/attr/validation.rs b/schemars_derive/src/attr/validation.rs
index a089a1de..ec7cc145 100644
--- a/schemars_derive/src/attr/validation.rs
+++ b/schemars_derive/src/attr/validation.rs
@@ -43,13 +43,17 @@ pub struct ValidationAttrs {
contains: Option,
required: bool,
format: Option,
+ inner: Option>,
}
impl ValidationAttrs {
pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self {
+ let schemars_items = get_meta_items(attrs, "schemars", errors, false);
+ let validate_items = get_meta_items(attrs, "validate", errors, true);
+
ValidationAttrs::default()
- .populate(attrs, "schemars", false, errors)
- .populate(attrs, "validate", true, errors)
+ .populate(schemars_items, "schemars", false, errors)
+ .populate(validate_items, "validate", true, errors)
}
pub fn required(&self) -> bool {
@@ -58,7 +62,7 @@ impl ValidationAttrs {
fn populate(
mut self,
- attrs: &[syn::Attribute],
+ meta_items: Vec,
attr_type: &'static str,
ignore_errors: bool,
errors: &Ctxt,
@@ -97,11 +101,7 @@ impl ValidationAttrs {
}
};
- for meta_item in attrs
- .iter()
- .flat_map(|attr| get_meta_items(attr, attr_type, errors, ignore_errors))
- .flatten()
- {
+ for meta_item in meta_items {
match &meta_item {
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("length") => {
for nested in meta_list.nested.iter() {
@@ -247,7 +247,8 @@ impl ValidationAttrs {
if !ignore_errors {
errors.error_spanned_by(
meta,
- "unknown item in schemars regex attribute".to_string(),
+ "unknown item in schemars regex attribute"
+ .to_string(),
);
}
}
@@ -292,7 +293,8 @@ impl ValidationAttrs {
if !ignore_errors {
errors.error_spanned_by(
meta,
- "unknown item in schemars contains attribute".to_string(),
+ "unknown item in schemars contains attribute"
+ .to_string(),
);
}
}
@@ -302,6 +304,21 @@ impl ValidationAttrs {
}
}
+ NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("inner") => {
+ match self.inner {
+ Some(_) => duplicate_error(&meta_list.path),
+ None => {
+ let inner_attrs = ValidationAttrs::default().populate(
+ meta_list.nested.clone().into_iter().collect(),
+ attr_type,
+ ignore_errors,
+ errors,
+ );
+ self.inner = Some(Box::new(inner_attrs));
+ }
+ }
+ }
+
_ => {}
}
}
@@ -309,16 +326,24 @@ impl ValidationAttrs {
}
pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) {
+ if let Some(apply_expr) = self.apply_to_schema_expr() {
+ *schema_expr = quote! {
+ {
+ let mut schema = #schema_expr;
+ #apply_expr
+ schema
+ }
+ }
+ }
+ }
+
+ fn apply_to_schema_expr(&self) -> Option {
let mut array_validation = Vec::new();
let mut number_validation = Vec::new();
let mut object_validation = Vec::new();
let mut string_validation = Vec::new();
- if let Some(length_min) = self
- .length_min
- .as_ref()
- .or(self.length_equal.as_ref())
- {
+ if let Some(length_min) = self.length_min.as_ref().or(self.length_equal.as_ref()) {
string_validation.push(quote! {
validation.min_length = Some(#length_min as u32);
});
@@ -327,11 +352,7 @@ impl ValidationAttrs {
});
}
- if let Some(length_max) = self
- .length_max
- .as_ref()
- .or(self.length_equal.as_ref())
- {
+ if let Some(length_max) = self.length_max.as_ref().or(self.length_equal.as_ref()) {
string_validation.push(quote! {
validation.max_length = Some(#length_max as u32);
});
@@ -378,6 +399,21 @@ impl ValidationAttrs {
}
});
+ let inner_validation = self
+ .inner
+ .as_deref()
+ .and_then(|inner| inner.apply_to_schema_expr())
+ .map(|apply_expr| {
+ quote! {
+ if schema_object.has_type(schemars::schema::InstanceType::Array) {
+ if let Some(schemars::schema::SingleOrVec::Single(inner_schema)) = &mut schema_object.array().items {
+ let mut schema = &mut **inner_schema;
+ #apply_expr
+ }
+ }
+ }
+ });
+
let array_validation = wrap_array_validation(array_validation);
let number_validation = wrap_number_validation(number_validation);
let object_validation = wrap_object_validation(object_validation);
@@ -388,21 +424,20 @@ impl ValidationAttrs {
|| object_validation.is_some()
|| string_validation.is_some()
|| format.is_some()
+ || inner_validation.is_some()
{
- *schema_expr = quote! {
- {
- let mut schema = #schema_expr;
- if let schemars::schema::Schema::Object(schema_object) = &mut schema
- {
- #array_validation
- #number_validation
- #object_validation
- #string_validation
- #format
- }
- schema
+ Some(quote! {
+ if let schemars::schema::Schema::Object(schema_object) = &mut schema {
+ #array_validation
+ #number_validation
+ #object_validation
+ #string_validation
+ #format
+ #inner_validation
}
- }
+ })
+ } else {
+ None
}
}
}