diff --git a/docs/1.1-attributes.md b/docs/1.1-attributes.md
index 317c51b8..52ddc8a2 100644
--- a/docs/1.1-attributes.md
+++ b/docs/1.1-attributes.md
@@ -49,6 +49,7 @@ TABLE OF CONTENTS
- [`deprecated`](#deprecated)
- [`crate`](#crate)
- [`extend`](#extend)
+ - [`transform`](#transform)
- [Doc Comments (`doc`)](#doc)
@@ -326,10 +327,31 @@ Set on a container, variant or field to add properties (or replace existing prop
The key must be a quoted string, and the value can be any expression that produces a type implementing `serde::Serialize`. The value can also be a JSON literal which can interpolate other values.
```plaintext
+#[derive(JsonSchema)]
#[schemars(extend("simple" = "string value", "complex" = {"array": [1, 2, 3]}))]
struct Struct;
```
+
+
+Set on a container, variant or field to run a `schemars::transform::Transform` against the generated schema. This can be specified multiple times to run multiple transforms.
+
+The `Transform` trait is implemented on functions with the signature `fn(&mut Schema) -> ()`, allowing you to do this:
+
+```rust
+fn my_transform(schema: &mut Schema) {
+ todo!()
+}
+
+#[derive(JsonSchema)]
+#[schemars(transform = my_transform)]
+struct Struct;
+```
+
Doc Comments (`#[doc = "..."]`)
diff --git a/docs/Gemfile b/docs/Gemfile
index d3cf985c..0888f7a2 100644
--- a/docs/Gemfile
+++ b/docs/Gemfile
@@ -24,7 +24,3 @@ install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do
gem "tzinfo", "~> 1.2"
gem "tzinfo-data"
end
-
-# Performance-booster for watching directories on Windows
-gem "wdm", "~> 0.1.1", :install_if => Gem.win_platform?
-
diff --git a/schemars/tests/expected/transform_enum_external.json b/schemars/tests/expected/transform_enum_external.json
new file mode 100644
index 00000000..af746e3c
--- /dev/null
+++ b/schemars/tests/expected/transform_enum_external.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "title": "External",
+ "oneOf": [
+ {
+ "type": "string",
+ "const": "Unit",
+ "propertyCount": 0,
+ "upperType": "STRING"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "NewType": true
+ },
+ "required": [
+ "NewType"
+ ],
+ "additionalProperties": false,
+ "propertyCount": 1,
+ "upperType": "OBJECT"
+ }
+ ],
+ "propertyCount": 0
+}
\ No newline at end of file
diff --git a/schemars/tests/expected/transform_struct.json b/schemars/tests/expected/transform_struct.json
new file mode 100644
index 00000000..6723414c
--- /dev/null
+++ b/schemars/tests/expected/transform_struct.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "title": "Struct",
+ "type": "object",
+ "properties": {
+ "value": true,
+ "int": {
+ "type": "integer",
+ "format": "int32",
+ "propertyCount": 0,
+ "upperType": "INTEGER"
+ }
+ },
+ "required": [
+ "value",
+ "int"
+ ],
+ "upperType": "OBJECT",
+ "propertyCount": 2
+}
\ No newline at end of file
diff --git a/schemars/tests/extend.rs b/schemars/tests/extend.rs
index 08f42fa9..2b44f5e0 100644
--- a/schemars/tests/extend.rs
+++ b/schemars/tests/extend.rs
@@ -17,7 +17,7 @@ struct Struct {
}
#[test]
-fn doc_comments_struct() -> TestResult {
+fn extend_struct() -> TestResult {
test_default_generated_schema::("extend_struct")
}
@@ -36,7 +36,7 @@ enum External {
}
#[test]
-fn doc_comments_enum_external() -> TestResult {
+fn extend_enum_external() -> TestResult {
test_default_generated_schema::("extend_enum_external")
}
@@ -53,7 +53,7 @@ enum Internal {
}
#[test]
-fn doc_comments_enum_internal() -> TestResult {
+fn extend_enum_internal() -> TestResult {
test_default_generated_schema::("extend_enum_internal")
}
@@ -72,7 +72,7 @@ enum Untagged {
}
#[test]
-fn doc_comments_enum_untagged() -> TestResult {
+fn extend_enum_untagged() -> TestResult {
test_default_generated_schema::("extend_enum_untagged")
}
@@ -91,6 +91,6 @@ enum Adjacent {
}
#[test]
-fn doc_comments_enum_adjacent() -> TestResult {
+fn extend_enum_adjacent() -> TestResult {
test_default_generated_schema::("extend_enum_adjacent")
}
diff --git a/schemars/tests/transform.rs b/schemars/tests/transform.rs
new file mode 100644
index 00000000..13ac30b1
--- /dev/null
+++ b/schemars/tests/transform.rs
@@ -0,0 +1,51 @@
+mod util;
+use schemars::{transform::RecursiveTransform, JsonSchema, Schema};
+use serde_json::Value;
+use util::*;
+
+fn capitalize_type(schema: &mut Schema) {
+ if let Some(obj) = schema.as_object_mut() {
+ if let Some(Value::String(ty)) = obj.get("type") {
+ obj.insert("upperType".to_owned(), ty.to_uppercase().into());
+ }
+ }
+}
+
+fn insert_property_count(schema: &mut Schema) {
+ if let Some(obj) = schema.as_object_mut() {
+ let count = obj
+ .get("properties")
+ .and_then(|p| p.as_object())
+ .map_or(0, |p| p.len());
+ obj.insert("propertyCount".to_owned(), count.into());
+ }
+}
+
+#[allow(dead_code)]
+#[derive(JsonSchema)]
+#[schemars(transform = RecursiveTransform(capitalize_type), transform = insert_property_count)]
+struct Struct {
+ value: Value,
+ #[schemars(transform = insert_property_count)]
+ int: i32,
+}
+
+#[test]
+fn transform_struct() -> TestResult {
+ test_default_generated_schema::("transform_struct")
+}
+
+#[allow(dead_code)]
+#[derive(JsonSchema)]
+#[schemars(transform = RecursiveTransform(capitalize_type), transform = insert_property_count)]
+enum External {
+ #[schemars(transform = insert_property_count)]
+ Unit,
+ #[schemars(transform = insert_property_count)]
+ NewType(Value),
+}
+
+#[test]
+fn transform_enum_external() -> TestResult {
+ test_default_generated_schema::("transform_enum_external")
+}
diff --git a/schemars/tests/ui/transform_str.rs b/schemars/tests/ui/transform_str.rs
new file mode 100644
index 00000000..6570accf
--- /dev/null
+++ b/schemars/tests/ui/transform_str.rs
@@ -0,0 +1,7 @@
+use schemars::JsonSchema;
+
+#[derive(JsonSchema)]
+#[schemars(transform = "x")]
+pub struct Struct;
+
+fn main() {}
diff --git a/schemars/tests/ui/transform_str.stderr b/schemars/tests/ui/transform_str.stderr
new file mode 100644
index 00000000..6ee36983
--- /dev/null
+++ b/schemars/tests/ui/transform_str.stderr
@@ -0,0 +1,6 @@
+error: Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`.
+ Did you mean `[schemars(transform = x)]`?
+ --> tests/ui/transform_str.rs:4:24
+ |
+4 | #[schemars(transform = "x")]
+ | ^^^
diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs
index 108cedc4..6b6e6766 100644
--- a/schemars_derive/src/attr/mod.rs
+++ b/schemars_derive/src/attr/mod.rs
@@ -27,6 +27,7 @@ pub struct Attrs {
pub crate_name: Option,
pub is_renamed: bool,
pub extensions: Vec<(String, TokenStream)>,
+ pub transforms: Vec,
}
#[derive(Debug)]
@@ -70,6 +71,7 @@ impl Attrs {
deprecated: self.deprecated,
examples: &self.examples,
extensions: &self.extensions,
+ transforms: &self.transforms,
read_only: false,
write_only: false,
default: None,
@@ -164,6 +166,25 @@ impl Attrs {
}
}
+ Meta::NameValue(m) if m.path.is_ident("transform") && attr_type == "schemars" => {
+ if let syn::Expr::Lit(syn::ExprLit {
+ lit: syn::Lit::Str(lit_str),
+ ..
+ }) = &m.value
+ {
+ if parse_lit_str::(lit_str).is_ok() {
+ errors.error_spanned_by(
+ &m.value,
+ format!(
+ "Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`.\nDid you mean `[schemars(transform = {})]`?",
+ lit_str.value()
+ ),
+ )
+ }
+ }
+ self.transforms.push(m.value.clone());
+ }
+
Meta::List(m) if m.path.is_ident("extend") && attr_type == "schemars" => {
let parser =
syn::punctuated::Punctuated::::parse_terminated;
@@ -224,7 +245,8 @@ impl Attrs {
crate_name: None,
is_renamed: _,
extensions,
- } if examples.is_empty() && extensions.is_empty())
+ transforms
+ } if examples.is_empty() && extensions.is_empty() && transforms.is_empty())
}
}
diff --git a/schemars_derive/src/metadata.rs b/schemars_derive/src/metadata.rs
index 6a3808c3..c6b5dfe7 100644
--- a/schemars_derive/src/metadata.rs
+++ b/schemars_derive/src/metadata.rs
@@ -1,4 +1,5 @@
use proc_macro2::TokenStream;
+use syn::spanned::Spanned;
#[derive(Debug, Clone)]
pub struct SchemaMetadata<'a> {
@@ -10,6 +11,7 @@ pub struct SchemaMetadata<'a> {
pub examples: &'a [syn::Path],
pub default: Option,
pub extensions: &'a [(String, TokenStream)],
+ pub transforms: &'a [syn::Expr],
}
impl<'a> SchemaMetadata<'a> {
@@ -23,6 +25,18 @@ impl<'a> SchemaMetadata<'a> {
schema
}}
}
+ if !self.transforms.is_empty() {
+ let apply_transforms = self.transforms.iter().map(|t| {
+ quote_spanned! {t.span()=>
+ schemars::transform::Transform::transform(&mut #t, &mut schema);
+ }
+ });
+ *schema_expr = quote! {{
+ let mut schema = #schema_expr;
+ #(#apply_transforms)*
+ schema
+ }};
+ }
}
fn make_setters(&self) -> Vec {