Repository: GREsau/schemars
Pull request #354 (2024-11-20 19:28:24)

Allow `example` attribute value to be any expression
```rust
#[schemars(example = 0.1)]
n: f32,
#[schemars(example = "example value", example = a_second_example())]
s: String,
```

Currently, the `example` attribute mirrors serde's `default` attribute, in that its value must be a string literal identifying a function to call. This requires consumers to define a function that usually just returns a constant value, which is really just boilerplate. "Default literals" is a popular feature request for serde (https://github.com/serde-rs/serde/issues/368), although it's a bit awkward to implement cleanly due to backward-compatibility.

But unlike serde, schemars is still in pre-v1, so it's easier to making occasional breaking changes, and this is a case where I'd rather cause an up-front breaking change to avoid a long-term API wart.

While I'm comfortable making this a breaking change, I'm concerned with the ambiguity that would arise from naively updating schemars from 0.8.x (or <v1.0.0-alpha.15) when using `example` - e.g. consider existing code:

```rust
#[derive(JsonSchema)]
pub struct MyStruct {
    #[schemars(example = "foo")]
    pub my_int: i32,
}

fn foo() -> i32 {
   123
}
```

This currently sets the `examples` schema property to `[123]`, but with the new behaviour, would still compile and run, but set the property to `["foo"]`. There would be no clear indication that they would need to change the attribute to either `#[schemars(example = foo())]` or `#[schemars(example = 123)]`.

Because of that, I think the safest way to make this change is to disallow string literals that contain a value parseable as a function path, which will cause a compile error until the attribute is updated. If you do want to set the example value to a such a string, then a simple workaround would be to borrow the string, e.g. `#[schemars(example = &"example")]` which would unambiguously refer to the string value instead of a function `example()`.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a84c477..7b0adea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,9 +6,10 @@
 
 - the `enumset1`/`enumset` optional dependency has been removed, as its `JsonSchema` impl did not actually match the default serialization format of `EnumSet` (https://github.com/GREsau/schemars/pull/339)
 
-### Changed
+### Changed (_⚠️ breaking changes ⚠️_)
 
-- ⚠️ MSRV is now 1.70 ⚠️
+- MSRV is now 1.70
+- [The `example` attribute](https://graham.cool/schemars/deriving/attributes/#example) value is now an arbitrary expression, rather than a string literal identifying a function to call. To avoid silent behaviour changes, the expression must not be a string literal where the value can be parsed as a function path - e.g. `#[schemars(example = "foo")]` is now a compile error, but `#[schemars(example = foo())]` is allowed (as is `#[schemars(example = &"foo")]` if you want the the literal string value `"foo"` to be the example).
 
 ### Fixed
 
diff --git a/docs/_includes/attributes.md b/docs/_includes/attributes.md
index 37f6833..1497b0f 100644
--- a/docs/_includes/attributes.md
+++ b/docs/_includes/attributes.md
@@ -298,13 +298,15 @@ Set on a container, variant or field to set the generated schema's `title` and/o
 
 <h3 id="example">
 
-`#[schemars(example = "some::function")]`
+`#[schemars(example = value)]`
 
 </h3>
 
-Set on a container, variant or field to include the result of the given function in the generated schema's `examples`. The function should take no parameters and can return any type that implements serde's `Serialize` trait - it does not need to return the same type as the attached struct/field. This attribute can be repeated to specify multiple examples.
+Set on a container, variant or field to include the given value in the generated schema's `examples`. The value can be any type that implements serde's `Serialize` trait - it does not need to be the same type as the attached struct/field. This attribute can be repeated to specify multiple examples.
 
-To use the result of arbitrary expressions as examples, you can instead use the [`extend`](#extend) attribute, e.g. `[schemars(extend("examples" = ["example string"]))]`.
+In previous versions of schemars, the value had to be a string literal identifying a defined function that would be called to return the actual example value (similar to the [`default`](#default) attribute). To avoid the new attribute behaviour from silently breaking old consumers, string literals consisting of a single word (e.g. `#[schemars(example = "my_fn")]`) or a path (e.g. `#[schemars(example = "my_mod::my_fn")]`) are currently disallowed. This restriction may be relaxed in a future version of schemars, but for now if you want to include such a string as the literal example value, this can be done by borrowing the value, e.g. `#[schemars(example = &"my_fn")]`. If you instead want to call a function to get the example value (mirrorring the old behaviour), you must use an explicit function call expression, e.g. `#[schemars(example = my_fn())]`.
+
+Alternatively, to directly set multiple examples without repeating `example = ...` attribute, you can instead use the [`extend`](#extend) attribute, e.g. `#[schemars(extend("examples" = [1, 2, 3]))]`.
 
 <h3 id="deprecated">
 
diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs
index 14fd6eb..37350d3 100644
--- a/schemars_derive/src/attr/mod.rs
+++ b/schemars_derive/src/attr/mod.rs
@@ -21,7 +21,7 @@ pub struct CommonAttrs {
     pub deprecated: bool,
     pub title: Option<Expr>,
     pub description: Option<Expr>,
-    pub examples: Vec<Path>,
+    pub examples: Vec<Expr>,
     pub extensions: Vec<(String, TokenStream)>,
     pub transforms: Vec<Expr>,
 }
@@ -84,7 +84,24 @@ impl CommonAttrs {
             },
 
             "example" => {
-                self.examples.extend(parse_name_value_lit_str(meta, cx));
+                if let Ok(expr) = parse_name_value_expr(meta, cx) {
+                    if let Expr::Lit(ExprLit {
+                        lit: Lit::Str(lit_str),
+                        ..
+                    }) = &expr
+                    {
+                        if lit_str.parse::<Path>().is_ok() {
+                            let lit_str_value = lit_str.value();
+                            cx.error_spanned_by(&expr, format_args!(
+                                "`example` value must be an expression, and string literals that may be interpreted as function paths are currently disallowed to avoid migration errors \
+                                 (this restriction may be relaxed in a future version of schemars).\n\
+                                If you want to use the result of a function, use `#[schemars(example = {lit_str_value}())]`.\n\
+                                Or to use the string literal value, use `#[schemars(example = &\"{lit_str_value}\")]`."));
+                        }
+                    }
+
+                    self.examples.push(expr);
+                }
             }
 
             "extend" => {
@@ -114,7 +131,7 @@ impl CommonAttrs {
                             cx.error_spanned_by(
                                 &expr,
                                 format_args!(
-                                    "Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`.\nDid you mean `[schemars(transform = {})]`?",
+                                    "Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`.\nDid you mean `#[schemars(transform = {})]`?",
                                     lit_str.value()
                                 ),
                             )
@@ -178,7 +195,7 @@ impl CommonAttrs {
         if !self.examples.is_empty() {
             let examples = self.examples.iter().map(|eg| {
                 quote! {
-                    schemars::_private::serde_json::value::to_value(#eg())
+                    schemars::_private::serde_json::value::to_value(#eg)
                 }
             });
             mutators.push(quote! {


Test patch:
diff --git a/schemars/tests/integration/examples.rs b/schemars/tests/integration/examples.rs
index 43b7419..427a666 100644
--- a/schemars/tests/integration/examples.rs
+++ b/schemars/tests/integration/examples.rs
@@ -1,21 +1,15 @@
 use crate::prelude::*;
 
 #[derive(Default, JsonSchema, Serialize)]
-#[schemars(example = "Struct::default", example = "null")]
+#[schemars(example = Struct::default(), example = ())]
 struct Struct {
-    #[schemars(example = "eight", example = "null")]
+    #[schemars(example = 4 + 4, example = ())]
     foo: i32,
     bar: bool,
-    #[schemars(example = "null")]
+    #[schemars(example = (), example = &"foo")]
     baz: Option<&'static str>,
 }
 
-fn eight() -> i32 {
-    8
-}
-
-fn null() {}
-
 #[test]
 fn examples() {
     test!(Struct).assert_snapshot();
diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.de.json b/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.de.json
index b891aa0..da04166 100644
--- a/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.de.json
+++ b/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.de.json
@@ -20,7 +20,8 @@
         "null"
       ],
       "examples": [
-        null
+        null,
+        "foo"
       ]
     }
   },
diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.ser.json b/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.ser.json
index 97465db..7de3378 100644
--- a/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.ser.json
+++ b/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.ser.json
@@ -20,7 +20,8 @@
         "null"
       ],
       "examples": [
-        null
+        null,
+        "foo"
       ]
     }
   },
diff --git a/schemars/tests/ui/example_fn.rs b/schemars/tests/ui/example_fn.rs
new file mode 100644
index 0000000..c4921c5
--- /dev/null
+++ b/schemars/tests/ui/example_fn.rs
@@ -0,0 +1,9 @@
+use schemars::JsonSchema;
+
+#[derive(JsonSchema)]
+#[schemars(example = "my_fn")]
+pub struct Struct;
+
+fn my_fn() {}
+
+fn main() {}
diff --git a/schemars/tests/ui/example_fn.stderr b/schemars/tests/ui/example_fn.stderr
new file mode 100644
index 0000000..92a34f3
--- /dev/null
+++ b/schemars/tests/ui/example_fn.stderr
@@ -0,0 +1,7 @@
+error: `example` value must be an expression, and string literals that may be interpreted as function paths are currently disallowed to avoid migration errors (this restriction may be relaxed in a future version of schemars).
+       If you want to use the result of a function, use `#[schemars(example = my_fn())]`.
+       Or to use the string literal value, use `#[schemars(example = &"my_fn")]`.
+ --> tests/ui/example_fn.rs:4:22
+  |
+4 | #[schemars(example = "my_fn")]
+  |                      ^^^^^^^
diff --git a/schemars/tests/ui/transform_str.stderr b/schemars/tests/ui/transform_str.stderr
index 6ee3698..50cf7b9 100644
--- a/schemars/tests/ui/transform_str.stderr
+++ b/schemars/tests/ui/transform_str.stderr
@@ -1,5 +1,5 @@
 error: Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`.
-       Did you mean `[schemars(transform = x)]`?
+       Did you mean `#[schemars(transform = x)]`?
  --> tests/ui/transform_str.rs:4:24
   |
 4 | #[schemars(transform = "x")]