Repository: juhaku/utoipa
Pull request #17 (2022-01-31 23:31:33)

Add response example and update other examples
* Add response example possibility with json!() or as literal string
* Update dependencies and extract serde_json to be optional allowing better grained control over dependencies
* Update struct with named fields example to use same json!() or literal string logic as response
* Add tests


Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 49dcc35..6f71144 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -26,7 +26,10 @@ jobs:
         key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
     
     - name: Run tests
-      run: cargo test 
+      run: |
+        cargo test
+        cargo test --test path_response_derive_test_no_serde_json --no-default-features
+        cargo test --test component_derive_no_serde_json --no-default-features
 
   test-with-actix:
     runs-on: ubuntu-latest
@@ -42,7 +45,7 @@ jobs:
           ~/.cargo/registry/cache/
           ~/.cargo/git/db/
           target/
-        key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+        key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-actix
     
     - name: Run tests
       run: cargo test --features actix-web,actix_extras
diff --git a/Cargo.toml b/Cargo.toml
index cc78d5a..e4f8980 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,16 +16,18 @@ exclude = [
 ]
 
 [features]
+default = ["json"]
 swagger_ui = ["rust-embed", "mime_guess"]
 debug = ["utoipa-gen/debug"]
 actix_extras = ["utoipa-gen/actix_extras"]
+json = ["serde_json", "utoipa-gen/json"]
 
 [dependencies]
-serde = { version = "1.0.130", features = ["derive"] }
+serde = { version = "1.0.136", features = ["derive"] }
 log = "0.4.14"
-serde_json = "1.0.68"
-rust-embed = { version = "6.2.0", optional = true, features = ["interpolate-folder-path"] }
-actix-web =  { version = "3.3.2", optional = true }
+serde_json = { version = "1.0.78", optional = true }
+rust-embed = { version = "6.3.0", optional = true, features = ["interpolate-folder-path"] }
+actix-web =  { version = "3.3.3", optional = true }
 mime_guess = { version = "2.0.3", optional = true }
 utoipa-gen = { version = "0.1.0-beta.0", path = "./utoipa-gen" }
 
diff --git a/src/error.rs b/src/error.rs
index 912821b..3288731 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -2,6 +2,7 @@ use std::fmt::Display;
 
 #[derive(Debug)]
 pub enum Error {
+    #[cfg(feature = "serde_json")]
     Serde(serde_json::Error),
     Io(std::io::Error),
     FromUtf8(std::string::FromUtf8Error),
@@ -10,6 +11,7 @@ pub enum Error {
 impl Display for Error {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
+            #[cfg(feature = "serde_json")]
             Self::Serde(error) => write!(f, "serde: {}", error),
             Self::Io(error) => write!(f, "io: {}", error),
             Self::FromUtf8(error) => write!(f, "from utf8: {}", error),
@@ -29,6 +31,7 @@ impl From<std::io::Error> for Error {
     }
 }
 
+#[cfg(feature = "serde_json")]
 impl From<serde_json::Error> for Error {
     fn from(serde: serde_json::Error) -> Self {
         Self::Serde(serde)
diff --git a/src/openapi/content.rs b/src/openapi/content.rs
index b4455a1..11f5476 100644
--- a/src/openapi/content.rs
+++ b/src/openapi/content.rs
@@ -1,17 +1,43 @@
 use serde::{Deserialize, Serialize};
 
+#[cfg(feature = "serde_json")]
+use serde_json::Value;
+
 use super::Component;
 
 #[derive(Serialize, Deserialize, Default, Clone)]
 #[non_exhaustive]
 pub struct Content {
     pub schema: Component,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[cfg(feature = "serde_json")]
+    pub example: Option<Value>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[cfg(not(feature = "serde_json"))]
+    pub example: Option<String>,
 }
 
 impl Content {
     pub fn new<I: Into<Component>>(schema: I) -> Self {
         Self {
             schema: schema.into(),
+            example: None,
         }
     }
+
+    #[cfg(feature = "serde_json")]
+    pub fn with_example(mut self, example: Value) -> Self {
+        self.example = Some(example);
+
+        self
+    }
+
+    #[cfg(not(feature = "serde_json"))]
+    pub fn with_example<S: Into<String>>(mut self, example: S) -> Self {
+        self.example = Some(example.into());
+
+        self
+    }
 }
diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs
index 24e6413..1a569ec 100644
--- a/src/openapi/mod.rs
+++ b/src/openapi/mod.rs
@@ -85,10 +85,12 @@ impl OpenApi {
         self
     }
 
+    #[cfg(feature = "serde_json")]
     pub fn to_json(&self) -> Result<String, Error> {
         serde_json::to_string(self).map_err(Error::Serde)
     }
 
+    #[cfg(feature = "serde_json")]
     pub fn to_pretty_json(&self) -> Result<String, Error> {
         serde_json::to_string_pretty(self).map_err(Error::Serde)
     }
@@ -172,6 +174,7 @@ impl Default for Required {
 }
 
 #[cfg(test)]
+#[cfg(feature = "serde_json")]
 mod tests {
 
     use crate::{error::Error, openapi::licence::Licence};
diff --git a/src/openapi/request_body.rs b/src/openapi/request_body.rs
index b4fa509..697ed60 100644
--- a/src/openapi/request_body.rs
+++ b/src/openapi/request_body.rs
@@ -2,7 +2,7 @@ use std::collections::HashMap;
 
 use serde::{Deserialize, Serialize};
 
-use super::{Component, Content, Required};
+use super::{Content, Required};
 
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Default, Clone)]
@@ -34,15 +34,9 @@ impl RequestBody {
         self
     }
 
-    pub fn with_content<S: AsRef<str>, C: Into<Component>>(
-        mut self,
-        content_type: S,
-        component: C,
-    ) -> Self {
-        self.content.insert(
-            content_type.as_ref().to_string(),
-            Content::new(component.into()),
-        );
+    pub fn with_content<S: AsRef<str>>(mut self, content_type: S, content: Content) -> Self {
+        self.content
+            .insert(content_type.as_ref().to_string(), content);
 
         self
     }
diff --git a/src/openapi/response.rs b/src/openapi/response.rs
index 11f4f4f..448593e 100644
--- a/src/openapi/response.rs
+++ b/src/openapi/response.rs
@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, HashMap};
 
 use serde::{Deserialize, Serialize};
 
-use super::{header::Header, Component, Content};
+use super::{header::Header, Content};
 
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Default, Clone)]
@@ -45,15 +45,9 @@ impl Response {
         }
     }
 
-    pub fn with_content<C: Into<Component>, S: AsRef<str>>(
-        mut self,
-        content_type: S,
-        component: C,
-    ) -> Self {
-        self.content.insert(
-            content_type.as_ref().to_string(),
-            Content::new(component.into()),
-        );
+    pub fn with_content<S: AsRef<str>>(mut self, content_type: S, content: Content) -> Self {
+        self.content
+            .insert(content_type.as_ref().to_string(), content);
 
         self
     }
diff --git a/src/openapi/schema.rs b/src/openapi/schema.rs
index b225dba..67358b1 100644
--- a/src/openapi/schema.rs
+++ b/src/openapi/schema.rs
@@ -1,6 +1,7 @@
 use std::collections::HashMap;
 
 use serde::{Deserialize, Serialize};
+#[cfg(feature = "serde_json")]
 use serde_json::Value;
 
 use super::Deprecated;
@@ -33,6 +34,7 @@ impl Schema {
 
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Default, Clone)]
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[serde(rename_all = "camelCase")]
 pub struct Component {
     #[serde(flatten, skip_serializing_if = "Option::is_none")]
@@ -52,6 +54,7 @@ pub struct Component {
 }
 
 #[derive(Default)]
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct OneOf {
     items: Vec<Component>,
 }
@@ -86,6 +89,7 @@ impl From<OneOf> for Component {
 }
 
 #[derive(Default, Serialize, Deserialize, Clone)]
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[serde(rename_all = "camelCase")]
 pub struct Property {
     #[serde(rename = "type")]
@@ -98,14 +102,24 @@ pub struct Property {
     description: Option<String>,
 
     #[serde(skip_serializing_if = "Option::is_none")]
+    #[cfg(feature = "serde_json")]
     default: Option<Value>,
 
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[cfg(not(feature = "serde_json"))]
+    default: Option<String>,
+
     #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
     enum_values: Option<Vec<String>>,
 
     #[serde(skip_serializing_if = "Option::is_none")]
+    #[cfg(not(feature = "serde_json"))]
     example: Option<String>,
 
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[cfg(feature = "serde_json")]
+    example: Option<Value>,
+
     #[serde(skip_serializing_if = "Option::is_none")]
     deprecated: Option<Deprecated>,
 
@@ -136,12 +150,20 @@ impl Property {
         self
     }
 
+    #[cfg(feature = "serde_json")]
     pub fn with_default(mut self, default: Value) -> Self {
         self.default = Some(default);
 
         self
     }
 
+    #[cfg(not(feature = "serde_json"))]
+    pub fn with_default<I: Into<String>>(mut self, default: I) -> Self {
+        self.default = Some(default.into());
+
+        self
+    }
+
     pub fn with_enum_values<S: AsRef<str>>(mut self, enum_values: &[S]) -> Self {
         self.enum_values = Some(
             enum_values
@@ -153,8 +175,16 @@ impl Property {
         self
     }
 
-    pub fn with_example<S: AsRef<str>>(mut self, example: S) -> Self {
-        self.example = Some(example.as_ref().to_string());
+    #[cfg(not(feature = "serde_json"))]
+    pub fn with_example<I: Into<String>>(mut self, example: I) -> Self {
+        self.example = Some(example.into());
+
+        self
+    }
+
+    #[cfg(feature = "serde_json")]
+    pub fn with_example(mut self, example: Value) -> Self {
+        self.example = Some(example);
 
         self
     }
@@ -191,6 +221,7 @@ impl ToArray for Property {}
 
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Default, Clone)]
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[serde(rename_all = "camelCase")]
 pub struct Object {
     #[serde(rename = "type")]
@@ -209,7 +240,12 @@ pub struct Object {
     deprecated: Option<Deprecated>,
 
     #[serde(skip_serializing_if = "Option::is_none")]
+    #[cfg(feature = "serde_json")]
     example: Option<Value>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[cfg(not(feature = "serde_json"))]
+    example: Option<String>,
 }
 
 impl Object {
@@ -248,11 +284,19 @@ impl Object {
         self
     }
 
+    #[cfg(feature = "serde_json")]
     pub fn with_example(mut self, example: Value) -> Self {
         self.example = Some(example);
 
         self
     }
+
+    #[cfg(not(feature = "serde_json"))]
+    pub fn with_example<I: Into<String>>(mut self, example: I) -> Self {
+        self.example = Some(example.into());
+
+        self
+    }
 }
 
 impl From<Object> for Component {
@@ -268,6 +312,7 @@ impl ToArray for Object {}
 
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Default, Clone)]
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Ref {
     #[serde(rename = "$ref")]
     ref_location: String,
@@ -298,6 +343,7 @@ impl ToArray for Ref {}
 
 #[non_exhaustive]
 #[derive(Serialize, Deserialize, Default, Clone)]
+#[cfg_attr(feature = "debug", derive(Debug))]
 #[serde(rename_all = "camelCase")]
 pub struct Array {
     #[serde(rename = "type")]
@@ -356,6 +402,7 @@ where
 }
 
 #[derive(Deserialize, Clone)]
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub enum ComponentType {
     Object,
     String,
@@ -388,6 +435,7 @@ impl Serialize for ComponentType {
 }
 
 #[derive(Deserialize, Clone)]
+#[cfg_attr(feature = "debug", derive(Debug))]
 pub enum ComponentFormat {
     Int32,
     Int64,
@@ -421,12 +469,14 @@ impl Serialize for ComponentFormat {
 
 #[cfg(test)]
 mod tests {
+    #[cfg(feature = "serde_json")]
     use serde_json::{json, Value};
 
     use super::*;
     use crate::openapi::*;
 
     #[test]
+    #[cfg(feature = "serde_json")]
     fn create_schema_serializes_json() -> Result<(), Error> {
         let openapi = OpenApi::new(Info::new("My api", "1.0.0"), Paths::new()).with_components(
             Schema::new()
@@ -526,6 +576,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg(feature = "serde_json")]
     fn derive_object_with_example() {
         let expected = r#"{"type":"object","example":{"age":20,"name":"bob the cat"}}"#;
         let json_value = Object::new().with_example(json!({"age": 20, "name": "bob the cat"}));
@@ -538,6 +589,7 @@ mod tests {
         );
     }
 
+    #[cfg(feature = "serde_json")]
     fn get_json_path<'a>(value: &'a Value, path: &str) -> &'a Value {
         path.split('.').into_iter().fold(value, |acc, fragment| {
             acc.get(fragment).unwrap_or(&serde_json::value::Value::Null)
diff --git a/utoipa-gen/Cargo.toml b/utoipa-gen/Cargo.toml
index 5d3bde9..e2f72f8 100644
--- a/utoipa-gen/Cargo.toml
+++ b/utoipa-gen/Cargo.toml
@@ -20,3 +20,4 @@ lazy_static = { version = "1.4.0", optional = true }
 [features]
 debug = ["syn/extra-traits"]
 actix_extras = ["regex", "lazy_static"]
+json = []
\ No newline at end of file
diff --git a/utoipa-gen/src/component/attr.rs b/utoipa-gen/src/component/attr.rs
index d38cf8a..a474105 100644
--- a/utoipa-gen/src/component/attr.rs
+++ b/utoipa-gen/src/component/attr.rs
@@ -1,12 +1,12 @@
-use proc_macro2::{Group, Ident, TokenStream};
+use proc_macro2::{Ident, TokenStream};
 use proc_macro_error::{abort, ResultExt};
-use quote::{quote, quote_spanned, ToTokens};
+use quote::{quote, ToTokens};
 use syn::{
     parse::{Parse, ParseBuffer},
     Attribute, Error, ExprPath, Lit, Token,
 };
 
-use crate::parse_utils;
+use crate::{parse_utils, Example};
 
 #[cfg_attr(feature = "debug", derive(Debug))]
 pub struct ComponentAttr<T>
@@ -29,25 +29,25 @@ where
 #[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Enum {
     default: Option<TokenStream>,
-    example: Option<String>,
+    example: Option<TokenStream>,
 }
 
 #[cfg_attr(feature = "debug", derive(Debug))]
 pub struct Struct {
-    example: Option<TokenStream>,
+    example: Option<Example>,
 }
 
 #[derive(Default)]
 #[cfg_attr(feature = "debug", derive(Debug))]
 pub struct UnnamedFieldStruct {
     default: Option<TokenStream>,
-    example: Option<String>,
+    example: Option<TokenStream>,
 }
 
 #[derive(Default)]
 #[cfg_attr(feature = "debug", derive(Debug))]
 pub struct NamedField {
-    example: Option<String>,
+    example: Option<TokenStream>,
     format: Option<ExprPath>,
     default: Option<TokenStream>,
     write_only: Option<bool>,
@@ -67,12 +67,12 @@ impl Parse for ComponentAttr<Enum> {
             match name {
                 "default" => {
                     enum_attr.default = Some(parse_utils::parse_next(input, || {
-                        parse_default_as_token_stream(input, name)
+                        parse_lit_or_fn_ref_as_token_stream(input, name)
                     }))
                 }
                 "example" => {
                     enum_attr.example = Some(parse_utils::parse_next(input, || {
-                        parse_lit_as_string(input, name, "unparseable example, expected literal")
+                        parse_lit_or_fn_ref_as_token_stream(input, name)
                     }))
                 }
                 _ => {
@@ -107,25 +107,11 @@ impl Parse for ComponentAttr<Struct> {
 
         match name {
             "example" => {
-                let tokens = parse_utils::parse_next(input, || {
-                    if input.peek(syn::Ident) && input.peek2(Token![!]) {
-                        input.parse::<Ident>().unwrap();
-                        input.parse::<Token![!]>().unwrap();
-
-                        Ok(input
-                            .parse::<Group>()
-                            .expect_or_abort("unparseable example, expected parenthesis"))
-                    } else {
-                        Err(Error::new(
-                            ident.span(),
-                            "unexpected example, expected json!(...)",
-                        ))
-                    }
-                })?;
+                let example = parse_utils::parse_next_lit_str_or_json_example(input, &ident);
 
                 Ok(Self {
                     inner: Struct {
-                        example: Some(tokens.stream()),
+                        example: Some(example),
                     },
                 })
             }
@@ -150,16 +136,12 @@ impl Parse for ComponentAttr<UnnamedFieldStruct> {
             match name {
                 "default" => {
                     unnamed_struct.default = Some(parse_utils::parse_next(input, || {
-                        parse_default_as_token_stream(input, name)
+                        parse_lit_or_fn_ref_as_token_stream(input, name)
                     }))
                 }
                 "example" => {
                     unnamed_struct.example = Some(parse_utils::parse_next(input, || {
-                        parse_lit_as_string(
-                            input,
-                            name,
-                            "unparseable example, expected literal string",
-                        )
+                        parse_lit_or_fn_ref_as_token_stream(input, name)
                     }))
                 }
                 _ => {
@@ -201,7 +183,7 @@ impl Parse for ComponentAttr<NamedField> {
             match name {
                 "example" => {
                     field.example = Some(parse_utils::parse_next(input, || {
-                        parse_lit_as_string(input, name, "unparseable example, expected literal")
+                        parse_lit_or_fn_ref_as_token_stream(input, name)
                     }));
                 }
                 "format" => {
@@ -220,7 +202,7 @@ impl Parse for ComponentAttr<NamedField> {
                 }
                 "default" => {
                     field.default = Some(parse_utils::parse_next(input, || {
-                        parse_default_as_token_stream(input, name)
+                        parse_lit_or_fn_ref_as_token_stream(input, name)
                     }))
                 }
                 "write_only" => field.write_only = Some(parse_utils::parse_bool_or_true(input)),
@@ -248,47 +230,45 @@ impl Parse for ComponentAttr<NamedField> {
     }
 }
 
-fn parse_lit_as_string(input: &ParseBuffer, field: &str, error_msg: &str) -> String {
-    let lit = &input.parse::<Lit>().expect_or_abort(error_msg);
-    match lit {
-        Lit::Bool(bool) => bool.value().to_string(),
-        Lit::Byte(byte) => byte.value().to_string(),
-        Lit::ByteStr(byte_str) => String::from_utf8(byte_str.value()).unwrap_or_else(|_| {
-            abort!(
-                input.span(),
-                format!("unparseable utf8 content in: {}", &field)
-            )
-        }),
-        Lit::Char(char) => char.value().to_string(),
-        Lit::Float(float) => float.base10_digits().to_string(),
-        Lit::Int(int) => int.base10_digits().to_string(),
-        Lit::Str(str) => str.value(),
-        Lit::Verbatim(_) => {
-            abort!(
-                input.span(),
-                format!("unparseable literal in field: {}", &field)
-            )
+fn parse_lit_or_fn_ref_as_token_stream(input: &ParseBuffer, name: &str) -> TokenStream {
+    if input.peek(Lit) {
+        let literal = input.parse::<Lit>().unwrap();
+
+        #[cfg(feature = "json")]
+        {
+            quote! {
+                serde_json::json!(#literal)
+            }
         }
-    }
-}
 
-fn parse_default_as_token_stream(input: &ParseBuffer, name: &str) -> TokenStream {
-    if input.peek(Lit) {
-        let literal = parse_lit_as_string(
-            input,
-            name,
-            &format!("unparseable {}, expected literal", name),
-        );
-        quote_spanned! {input.span()=>
-            #literal
+        #[cfg(not(feature = "json"))]
+        {
+            quote! {
+                format!("{}", #literal)
+            }
         }
     } else {
-        let method = input.parse::<ExprPath>().expect_or_abort(&format!(
-            "unparseable {}, expected literal or expresssion path",
-            name
-        ));
-        quote_spanned! {input.span()=>
-            #method()
+        let method = input.parse::<ExprPath>().unwrap_or_else(|error| {
+            let message = &format!("unparseable {}, expected literal or expresssion path", name);
+            abort! {
+                error.span(), message;
+                help = "Try to define {} = value", name;
+                help = r#"You should define either literal value e.g. {} = 1 or {} = "value""#, name, name;
+                help = r#"You can also use function reference e.g {} = String::default"#, name
+            }
+        });
+
+        #[cfg(feature = "json")]
+        {
+            quote! {
+                serde_json::json!(#method())
+            }
+        }
+        #[cfg(not(feature = "json"))]
+        {
+            quote! {
+                format!("{}", #method())
+            }
         }
     }
 }
@@ -313,7 +293,7 @@ impl ToTokens for Enum {
     fn to_tokens(&self, tokens: &mut TokenStream) {
         if let Some(ref default) = self.default {
             tokens.extend(quote! {
-                .with_default(serde_json::json!(#default))
+                .with_default(#default)
             })
         }
 
@@ -329,7 +309,7 @@ impl ToTokens for Struct {
     fn to_tokens(&self, tokens: &mut TokenStream) {
         if let Some(ref example) = self.example {
             tokens.extend(quote! {
-                .with_example(serde_json::json!(#example))
+                .with_example(#example)
             })
         }
     }
@@ -339,7 +319,7 @@ impl ToTokens for UnnamedFieldStruct {
     fn to_tokens(&self, tokens: &mut TokenStream) {
         if let Some(ref default) = self.default {
             tokens.extend(quote! {
-                .with_default(serde_json::json!(#default))
+                .with_default(#default)
             })
         }
 
@@ -355,7 +335,7 @@ impl ToTokens for NamedField {
     fn to_tokens(&self, tokens: &mut TokenStream) {
         if let Some(ref default) = self.default {
             tokens.extend(quote! {
-                .with_default(serde_json::json!(#default))
+                .with_default(#default)
             })
         }
 
diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs
index 4b45720..33bafdb 100644
--- a/utoipa-gen/src/lib.rs
+++ b/utoipa-gen/src/lib.rs
@@ -258,10 +258,31 @@ impl Parse for Type {
     }
 }
 
+#[cfg_attr(feature = "debug", derive(Debug))]
+enum Example {
+    String(TokenStream2),
+    Json(TokenStream2),
+}
+
+impl ToTokens for Example {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        match self {
+            Self::Json(json) => tokens.extend(quote! {
+                serde_json::json!(#json)
+            }),
+            Self::String(string) => tokens.extend(string.to_owned()),
+        }
+    }
+}
+
 /// Parsing utils
 mod parse_utils {
-    use proc_macro_error::ResultExt;
-    use syn::{parse::ParseStream, LitBool, LitStr, Token};
+    use proc_macro2::{Group, Ident, TokenStream};
+    use proc_macro_error::{abort, ResultExt};
+    use quote::ToTokens;
+    use syn::{parse::ParseStream, Error, LitBool, LitStr, Token};
+
+    use crate::Example;
 
     pub fn parse_next<T: Sized>(input: ParseStream, next: impl FnOnce() -> T) -> T {
         input
@@ -288,4 +309,42 @@ mod parse_utils {
             true
         }
     }
+
+    pub fn parse_json_token_stream(input: ParseStream) -> Result<TokenStream, Error> {
+        if input.peek(syn::Ident) && input.peek2(Token![!]) {
+            input.parse::<Ident>().unwrap();
+            input.parse::<Token![!]>().unwrap();
+
+            Ok(input.parse::<Group>()?.stream())
+        } else {
+            Err(Error::new(
+                input.span(),
+                "unexpected token, expected json!(...)",
+            ))
+        }
+    }
+
+    fn parse_next_lit_str_or_json(input: ParseStream, abort_op: impl FnOnce(&Error)) -> Example {
+        if input.peek2(LitStr) {
+            Example::String(parse_next(input, || {
+                input.parse::<LitStr>().unwrap().to_token_stream()
+            }))
+        } else {
+            Example::Json(parse_next(input, || {
+                parse_json_token_stream(input).unwrap_or_else(|error| {
+                    abort_op(&error);
+                    // hacky way to tell rust that we are having a "never" type here
+                    unreachable!("oops! unreachable code we should have aborted here");
+                })
+            }))
+        }
+    }
+
+    pub(crate) fn parse_next_lit_str_or_json_example(input: ParseStream, ident: &Ident) -> Example {
+        parse_next_lit_str_or_json(input, |error| {
+            abort! {ident, "unparseable example, expected json!(), {}", error;
+            help = r#"Try defining example = json!({{"key": "value"}})"#;
+            }
+        })
+    }
 }
diff --git a/utoipa-gen/src/path/request_body.rs b/utoipa-gen/src/path/request_body.rs
index 5952fd6..a1b11f5 100644
--- a/utoipa-gen/src/path/request_body.rs
+++ b/utoipa-gen/src/path/request_body.rs
@@ -135,7 +135,7 @@ impl ToTokens for RequestBodyAttr {
 
             tokens.extend(quote! {
                 utoipa::openapi::request_body::RequestBody::new()
-                    .with_content(#content_type, #property)
+                    .with_content(#content_type, utoipa::openapi::Content::new(#property))
                     .with_required(#required)
             });
         }
diff --git a/utoipa-gen/src/path/response.rs b/utoipa-gen/src/path/response.rs
index 63241e1..b37f817 100644
--- a/utoipa-gen/src/path/response.rs
+++ b/utoipa-gen/src/path/response.rs
@@ -5,7 +5,7 @@ use syn::{
     bracketed, parse::Parse, punctuated::Punctuated, token::Comma, Error, LitInt, LitStr, Token,
 };
 
-use crate::{parse_utils, Type};
+use crate::{parse_utils, Example, Type};
 
 use super::{property::Property, ContentTypeResolver};
 
@@ -69,6 +69,7 @@ pub struct Response {
     response_type: Option<Type>,
     content_type: Option<String>,
     headers: Vec<Header>,
+    example: Option<Example>,
 }
 
 impl Parse for Response {
@@ -121,6 +122,11 @@ impl Parse for Response {
                         .map(|group| syn::parse2::<Header>(group.stream()).unwrap_or_abort())
                         .collect::<Vec<_>>();
                 }
+                "example" => {
+                    response.example = Some(parse_utils::parse_next_lit_str_or_json_example(
+                        input, &ident,
+                    ));
+                }
                 _ => {
                     let error_msg = format!(
                         "unexpected identifer: {}, expected any of: status, description, body, content_type, headers",
@@ -169,8 +175,17 @@ impl ToTokens for Responses<'_> {
                     &component.component_type,
                 );
 
+                let mut content = quote! {
+                    utoipa::openapi::Content::new(#component)
+                };
+                if let Some(ref example) = response.example {
+                    content.extend(quote! {
+                        .with_example(#example)
+                    })
+                }
+
                 response_tokens.extend(quote! {
-                    .with_content(#content_type, #component)
+                    .with_content(#content_type, #content)
                 })
             }
 


Test patch:
diff --git a/tests/common.rs b/tests/common.rs
index b6077cd..7abccba 100644
--- a/tests/common.rs
+++ b/tests/common.rs
@@ -1,3 +1,4 @@
+#![cfg(feature = "serde_json")]
 use serde_json::Value;
 
 pub fn get_json_path<'a>(value: &'a Value, path: &str) -> &'a Value {
diff --git a/tests/component_derive_no_serde_json.rs b/tests/component_derive_no_serde_json.rs
new file mode 100644
index 0000000..6ee4f3f
--- /dev/null
+++ b/tests/component_derive_no_serde_json.rs
@@ -0,0 +1,23 @@
+#![cfg(not(feature = "serde_json"))]
+
+use std::{print, println};
+
+use utoipa::Component;
+
+#[test]
+fn derive_component_with_string_example_compiles_success() {
+    #[derive(Component)]
+    #[component(example = r#"{"foo": "bar"}"#)]
+    struct Foo {
+        bar: String,
+    }
+}
+
+#[test]
+fn derive_component_with_string_example_attributes_compiles_success() {
+    #[derive(Component)]
+    struct Foo {
+        #[component(example = r#""bar""#, default = r#""foobar""#)]
+        bar: String,
+    }
+}
diff --git a/tests/component_derive_test.rs b/tests/component_derive_test.rs
index 712fae6..0b72792 100644
--- a/tests/component_derive_test.rs
+++ b/tests/component_derive_test.rs
@@ -1,3 +1,4 @@
+#![cfg(feature = "serde_json")]
 use std::{borrow::Cow, cell::RefCell, collections::HashMap, vec};
 
 use serde_json::Value;
@@ -168,7 +169,7 @@ fn derive_struct_with_optional_properties_success() {
         "type" = r#""object""#, "Owner type"
         "properties.id.type" = r#""integer""#, "Owner id type"
         "properties.id.format" = r#""int64""#, "Owner id format"
-        "properties.id.default" = r#""1""#, "Owner id default"
+        "properties.id.default" = r#"1"#, "Owner id default"
         "properties.enabled.type" = r#""boolean""#, "Owner enabled"
         "properties.books.type" = r#""array""#, "Owner books"
         "properties.books.items.$ref" = r###""#/components/schemas/Book""###, "Owner books items ref"
@@ -391,7 +392,7 @@ fn derive_unnamed_struct_deprecated_success() {
 
     assert_value! {pet_age=>
         "deprecated" = r#"true"#, "PetAge deprecated"
-        "example" = r#""8""#, "PetAge example"
+        "example" = r#"8"#, "PetAge example"
     }
 }
 
diff --git a/tests/path_derive.rs b/tests/path_derive.rs
index 8b52061..e6020b4 100644
--- a/tests/path_derive.rs
+++ b/tests/path_derive.rs
@@ -1,3 +1,4 @@
+#![cfg(feature = "serde_json")]
 use paste::paste;
 
 mod common;
diff --git a/tests/path_derive_actix.rs b/tests/path_derive_actix.rs
index cf829ae..b991600 100644
--- a/tests/path_derive_actix.rs
+++ b/tests/path_derive_actix.rs
@@ -1,4 +1,5 @@
 #![cfg(feature = "actix_extras")]
+#![cfg(feature = "serde_json")]
 
 use serde_json::Value;
 use utoipa::OpenApi;
diff --git a/tests/path_parameter_derive_actix.rs b/tests/path_parameter_derive_actix.rs
index 85d7bcc..ad23561 100644
--- a/tests/path_parameter_derive_actix.rs
+++ b/tests/path_parameter_derive_actix.rs
@@ -1,4 +1,5 @@
 #![cfg(feature = "actix_extras")]
+#![cfg(feature = "serde_json")]
 
 use utoipa::OpenApi;
 
diff --git a/tests/path_parameter_derive_test.rs b/tests/path_parameter_derive_test.rs
index cec8c5d..f477329 100644
--- a/tests/path_parameter_derive_test.rs
+++ b/tests/path_parameter_derive_test.rs
@@ -1,4 +1,5 @@
 #![cfg(not(feature = "actix_extras"))]
+#![cfg(feature = "serde_json")]
 
 use utoipa::OpenApi;
 
diff --git a/tests/path_response_derive_test.rs b/tests/path_response_derive_test.rs
index e2fa864..adab0b9 100644
--- a/tests/path_response_derive_test.rs
+++ b/tests/path_response_derive_test.rs
@@ -1,10 +1,11 @@
+#![cfg(feature = "serde_json")]
 mod common;
 
 macro_rules! test_fn {
-    ( module: $name:ident, responses: $responses:expr ) => {
+    ( module: $name:ident, responses: $($responses:tt)* ) => {
         #[allow(unused)]
         mod $name {
-            #[utoipa::path(get,path = "/foo",responses = $responses)]
+            #[utoipa::path(get,path = "/foo",responses = $($responses)*)]
             fn get_foo() {}
         }
     };
@@ -81,7 +82,8 @@ macro_rules! test_response_types {
                         (status = 200, description = "success",
                             $(body = $expected ,)*
                             $(content_type = $content_type,)*
-                            $(headers = $headers)*),
+                            $(headers = $headers, )*
+                        ),
                     ]
                 }
             }
@@ -173,3 +175,22 @@ response_no_body_with_complex_header_with_description => headers: [
     "responses.200.headers.random-digits.schema.items.type" = r###""integer""###, "random-digits header items type"
     "responses.200.headers.random-digits.schema.items.format" = r###""int64""###, "random-digits header items format"
 }
+
+test_fn! {
+    module: response_with_json_example,
+    responses: [
+        (status = 200, description = "success", body = Foo, example = json!({"foo": "bar"}))
+    ]
+}
+
+#[test]
+fn derive_response_with_json_example_success() {
+    let doc = api_doc!(module: response_with_json_example);
+
+    assert_value! {doc=>
+        "responses.200.description" = r#""success""#, "Response description"
+        "responses.200.content.application/json.schema.$ref" = r###""#/components/schemas/Foo""###, "Response content ref"
+        "responses.200.content.application/json.example" = r###"{"foo":"bar"}"###, "Response content example"
+        "responses.200.headers" = r#"null"#, "Response headers"
+    }
+}
diff --git a/tests/path_response_derive_test_no_serde_json.rs b/tests/path_response_derive_test_no_serde_json.rs
new file mode 100644
index 0000000..4b059f4
--- /dev/null
+++ b/tests/path_response_derive_test_no_serde_json.rs
@@ -0,0 +1,28 @@
+#![cfg(not(feature = "serde_json"))]
+
+macro_rules! test_fn {
+    ( module: $name:ident, responses: $($responses:tt)* ) => {
+        #[allow(unused)]
+        mod $name {
+            #[utoipa::path(get,path = "/foo",responses = $($responses)*)]
+            fn get_foo() {}
+        }
+    }
+}
+
+test_fn! {
+    module: response_with_string_example,
+    responses: [
+        (status = 200, description = "success", body = Foo, example = r#"{"foo": "bar"}"#)
+    ]
+}
+
+#[test]
+fn derive_response_with_string_example_compiles_success() {
+    use utoipa::OpenApi;
+
+    #[derive(OpenApi, Default)]
+    #[openapi(handlers = [response_with_string_example::get_foo])]
+    struct ApiDoc;
+    ApiDoc::openapi();
+}
diff --git a/tests/request_body_derive_test.rs b/tests/request_body_derive_test.rs
index aea6d52..f98995d 100644
--- a/tests/request_body_derive_test.rs
+++ b/tests/request_body_derive_test.rs
@@ -1,3 +1,4 @@
+#![cfg(feature = "serde_json")]
 use utoipa::OpenApi;
 
 mod common;