Repository: GREsau/schemars
Pull request #30 (2020-05-14 01:07:27)

"additionalProperties: false" for struct annotated with serde "#[serde(deny_unknown_fields)]"
As discussed in #27 as well as `TODO` in the code.

If a struct is annotated with `#[serde(deny_unknown_fields)]`, the schema should have `additionalProperties: false`.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9deeacf..fc35f30 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,12 @@
 # Changelog
 
-## In-dev - version TBC
+## [0.7.3] - 2020-05-15
+### Added:
+- `#[schemars(schema_with = "...")]` attribute can be set on variants and fields. This allows you to specify another function which returns the schema you want, which is particularly useful on fields of types that don't implement the JsonSchema trait (https://github.com/GREsau/schemars/issues/15)
+
 ### Fixed
 - `#[serde(with = "...")]`/`#[schemars(with = "...")]` attributes on enum variants are now respected
+- Some compiler errors generated by schemars_derive should now have more accurate spans
 
 ## [0.7.2] - 2020-04-30
 ### Added:
diff --git a/docs/1.1-attributes.md b/docs/1.1-attributes.md
index 9b44aee..6ff7fee 100644
--- a/docs/1.1-attributes.md
+++ b/docs/1.1-attributes.md
@@ -30,6 +30,7 @@ Serde also allows setting `#[serde(...)]` attributes which change how types are
     - [`flatten`](#flatten)
     - [`with`](#with)
 1. [Other Attributes](#other-attributes)
+    - [`schema_with`](#schema_with)
     - [Doc Comments (`doc`)](#doc)
 
 ## Supported Serde Attributes
@@ -117,16 +118,23 @@ Serde docs: [field](https://serde.rs/field-attrs.html#flatten)
 `#[serde(with = "Type")]` / `#[schemars(with = "Type")]`
 </h3>
 
-Set on a field to generate this field's schema as the given type instead of the field's actual type. Serde allows the `with` attribute to refer to any module path, but Schemars requires this to be an actual type which implements `JsonSchema`.
+Set on a variant or field to generate its schema as the given type instead of its actual type. Serde allows the `with` attribute to refer to any module path, but Schemars requires this to be an actual type which implements `JsonSchema`.
 
 If the given type has any required generic type parameters, then they must all be explicitly specified in this attribute. Serde frequently allows you to omit them as it can make use of type inference, but unfortunately this is not possible with Schemars. For example, `with = "Vec::<i32>"` will work, but `with = "Vec"` and `with = "Vec::<_>"` will not.
 
-Serde docs: [field](https://serde.rs/field-attrs.html#with)
+Serde docs: [variant](https://serde.rs/variant-attrs.html#with) / [field](https://serde.rs/field-attrs.html#with)
 
 </div>
 
 ## Other Attributes
 
+<h3 id="schema_with">
+
+`#[schemars(schema_with = "some::function")]`
+</h3>
+
+Set on a variant or field to generate this field's schema using the given function. This function must be callable as `fn(&mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema`.
+
 <h3 id="doc">
 
 Doc Comments (`#[doc = "..."]`)
diff --git a/docs/_includes/examples/custom_serialization.rs b/docs/_includes/examples/custom_serialization.rs
index 9429bc3..53c78fa 100644
--- a/docs/_includes/examples/custom_serialization.rs
+++ b/docs/_includes/examples/custom_serialization.rs
@@ -1,4 +1,5 @@
-use schemars::{schema_for, JsonSchema};
+use schemars::schema::{Schema, SchemaObject};
+use schemars::{gen::SchemaGenerator, schema_for, JsonSchema};
 use serde::{Deserialize, Serialize};
 
 // `int_as_string` and `bool_as_string` use the schema for `String`.
@@ -7,15 +8,24 @@ pub struct MyStruct {
     #[serde(default = "eight", with = "as_string")]
     #[schemars(with = "String")]
     pub int_as_string: i32,
+
     #[serde(default = "eight")]
     pub int_normal: i32,
+
     #[serde(default, with = "as_string")]
-    #[schemars(with = "String")]
+    #[schemars(schema_with = "make_custom_schema")]
     pub bool_as_string: bool,
+
     #[serde(default)]
     pub bool_normal: bool,
 }
 
+fn make_custom_schema(gen: &mut SchemaGenerator) -> Schema {
+    let mut schema: SchemaObject = <String>::json_schema(gen).into();
+    schema.format = Some("boolean".to_owned());
+    schema.into()
+}
+
 fn eight() -> i32 {
     8
 }
diff --git a/docs/_includes/examples/custom_serialization.schema.json b/docs/_includes/examples/custom_serialization.schema.json
index 017a143..42fda99 100644
--- a/docs/_includes/examples/custom_serialization.schema.json
+++ b/docs/_includes/examples/custom_serialization.schema.json
@@ -5,7 +5,8 @@
   "properties": {
     "bool_as_string": {
       "default": "false",
-      "type": "string"
+      "type": "string",
+      "format": "boolean"
     },
     "bool_normal": {
       "default": false,
diff --git a/docs/examples/7-custom_serialization.md b/docs/examples/7-custom_serialization.md
index 7005262..8caa930 100644
--- a/docs/examples/7-custom_serialization.md
+++ b/docs/examples/7-custom_serialization.md
@@ -13,7 +13,7 @@ summary: >-
 
 Serde allows you to change how a field is (de)serialized by setting a [`#[serde(with = "path")]`](https://serde.rs/field-attrs.html#with) attribute, where `$path::serialize` and `$path::deserialize` must be functions with the correct signature. Schemars supports the same attribute, but `path` must be a type implementing `JsonSchema`.
 
-In order to derive `JsonSchema` on a type which includes a `#[serde(with = "path")]` attribute where `path` is not  a type implementing `JsonSchema`, you'll need to override it with a suitable `#[schemars(with = "Type")]` attribute.
+In order to derive `JsonSchema` on a type which includes a `#[serde(with = "path")]` attribute where `path` is not  a type implementing `JsonSchema`, you'll need to override it with a suitable `#[schemars(with = "Type")]` or `#[schemars(schema_with = "path")]` attribute.
 
 {% include example.md name="custom_serialization" %}
 
diff --git a/schemars/Cargo.toml b/schemars/Cargo.toml
index 61a0c46..81b20e6 100644
--- a/schemars/Cargo.toml
+++ b/schemars/Cargo.toml
@@ -3,7 +3,7 @@ name = "schemars"
 description = "Generate JSON Schemas from Rust code"
 homepage = "https://graham.cool/schemars/"
 repository = "https://github.com/GREsau/schemars"
-version = "0.7.2"
+version = "0.7.3"
 authors = ["Graham Esau <gesau@hotmail.co.uk>"]
 edition = "2018"
 license = "MIT"
@@ -13,7 +13,7 @@ categories = ["encoding"]
 build = "build.rs"
 
 [dependencies]
-schemars_derive = { version = "=0.7.2", path = "../schemars_derive" }
+schemars_derive = { version = "=0.7.3", path = "../schemars_derive" }
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 
diff --git a/schemars/examples/custom_serialization.rs b/schemars/examples/custom_serialization.rs
index 9429bc3..53c78fa 100644
--- a/schemars/examples/custom_serialization.rs
+++ b/schemars/examples/custom_serialization.rs
@@ -1,4 +1,5 @@
-use schemars::{schema_for, JsonSchema};
+use schemars::schema::{Schema, SchemaObject};
+use schemars::{gen::SchemaGenerator, schema_for, JsonSchema};
 use serde::{Deserialize, Serialize};
 
 // `int_as_string` and `bool_as_string` use the schema for `String`.
@@ -7,15 +8,24 @@ pub struct MyStruct {
     #[serde(default = "eight", with = "as_string")]
     #[schemars(with = "String")]
     pub int_as_string: i32,
+
     #[serde(default = "eight")]
     pub int_normal: i32,
+
     #[serde(default, with = "as_string")]
-    #[schemars(with = "String")]
+    #[schemars(schema_with = "make_custom_schema")]
     pub bool_as_string: bool,
+
     #[serde(default)]
     pub bool_normal: bool,
 }
 
+fn make_custom_schema(gen: &mut SchemaGenerator) -> Schema {
+    let mut schema: SchemaObject = <String>::json_schema(gen).into();
+    schema.format = Some("boolean".to_owned());
+    schema.into()
+}
+
 fn eight() -> i32 {
     8
 }
diff --git a/schemars/examples/custom_serialization.schema.json b/schemars/examples/custom_serialization.schema.json
index 017a143..42fda99 100644
--- a/schemars/examples/custom_serialization.schema.json
+++ b/schemars/examples/custom_serialization.schema.json
@@ -5,7 +5,8 @@
   "properties": {
     "bool_as_string": {
       "default": "false",
-      "type": "string"
+      "type": "string",
+      "format": "boolean"
     },
     "bool_normal": {
       "default": false,
diff --git a/schemars_derive/Cargo.toml b/schemars_derive/Cargo.toml
index 1b9f38e..f6d396b 100644
--- a/schemars_derive/Cargo.toml
+++ b/schemars_derive/Cargo.toml
@@ -3,7 +3,7 @@ name = "schemars_derive"
 description = "Macros for #[derive(JsonSchema)], for use with schemars"
 homepage = "https://graham.cool/schemars/"
 repository = "https://github.com/GREsau/schemars"
-version = "0.7.2"
+version = "0.7.3"
 authors = ["Graham Esau <gesau@hotmail.co.uk>"]
 edition = "2018"
 license = "MIT"
diff --git a/schemars_derive/src/ast/mod.rs b/schemars_derive/src/ast/mod.rs
index e0ac3fc..2f2cb98 100644
--- a/schemars_derive/src/ast/mod.rs
+++ b/schemars_derive/src/ast/mod.rs
@@ -1,6 +1,6 @@
 mod from_serde;
 
-use crate::attr::{Attrs, WithAttr};
+use crate::attr::Attrs;
 use from_serde::FromSerde;
 use serde_derive_internals::ast as serde_ast;
 use serde_derive_internals::{Ctxt, Derive};
@@ -68,12 +68,4 @@ impl<'a> Field<'a> {
     pub fn name(&self) -> String {
         self.serde_attrs.name().deserialize_name()
     }
-
-    pub fn type_for_schema(&self) -> &syn::Type {
-        match &self.attrs.with {
-            None => self.ty,
-            Some(WithAttr::Type(ty)) => ty,
-            Some(WithAttr::_Function(_)) => unimplemented!(), // TODO
-        }
-    }
 }
diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs
index ebcd7bd..6e36946 100644
--- a/schemars_derive/src/attr/mod.rs
+++ b/schemars_derive/src/attr/mod.rs
@@ -22,7 +22,7 @@ pub struct Attrs {
 #[derive(Debug)]
 pub enum WithAttr {
     Type(syn::Type),
-    _Function(syn::Path),
+    Function(syn::Path),
 }
 
 impl Attrs {
@@ -74,14 +74,22 @@ impl Attrs {
                     if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.lit) {
                         match self.with {
                             Some(WithAttr::Type(_)) => duplicate_error(m),
-                            Some(WithAttr::_Function(_)) => {
-                                mutual_exclusive_error(m, "schema_with")
-                            }
+                            Some(WithAttr::Function(_)) => mutual_exclusive_error(m, "schema_with"),
                             None => self.with = Some(WithAttr::Type(ty)),
                         }
                     }
                 }
 
+                Meta(NameValue(m)) if m.path.is_ident("schema_with") => {
+                    if let Ok(fun) = parse_lit_into_path(errors, attr_type, "schema_with", &m.lit) {
+                        match self.with {
+                            Some(WithAttr::Function(_)) => duplicate_error(m),
+                            Some(WithAttr::Type(_)) => mutual_exclusive_error(m, "with"),
+                            None => self.with = Some(WithAttr::Function(fun)),
+                        }
+                    }
+                }
+
                 Meta(_meta_item) => {
                     // TODO uncomment this for 0.8.0 (breaking change)
                     //  https://github.com/GREsau/schemars/issues/18
@@ -148,8 +156,8 @@ fn get_lit_str<'a>(
         cx.error_spanned_by(
             lit,
             format!(
-                "expected {} attribute to be a string: `{} = \"...\"`",
-                attr_type, meta_item_name
+                "expected {} {} attribute to be a string: `{} = \"...\"`",
+                attr_type, meta_item_name, meta_item_name
             ),
         );
         Err(())
@@ -167,7 +175,31 @@ fn parse_lit_into_ty(
     parse_lit_str(string).map_err(|_| {
         cx.error_spanned_by(
             lit,
-            format!("failed to parse type: {} = {:?}", attr_type, string.value()),
+            format!(
+                "failed to parse type: `{} = {:?}`",
+                meta_item_name,
+                string.value()
+            ),
+        )
+    })
+}
+
+fn parse_lit_into_path(
+    cx: &Ctxt,
+    attr_type: &'static str,
+    meta_item_name: &'static str,
+    lit: &syn::Lit,
+) -> Result<syn::Path, ()> {
+    let string = get_lit_str(cx, attr_type, meta_item_name, lit)?;
+
+    parse_lit_str(string).map_err(|_| {
+        cx.error_spanned_by(
+            lit,
+            format!(
+                "failed to parse path: `{} = {:?}`",
+                meta_item_name,
+                string.value()
+            ),
         )
     })
 }
diff --git a/schemars_derive/src/attr/schemars_to_serde.rs b/schemars_derive/src/attr/schemars_to_serde.rs
index 22042d2..20b7b10 100644
--- a/schemars_derive/src/attr/schemars_to_serde.rs
+++ b/schemars_derive/src/attr/schemars_to_serde.rs
@@ -8,8 +8,7 @@ use syn::{Attribute, Data, Field, Meta, NestedMeta, Variant};
 static SERDE_KEYWORDS: &[&str] = &[
     "rename",
     "rename_all",
-    // TODO: for structs with `deny_unknown_fields`, set schema's `additionalProperties` to false.
-    // "deny_unknown_fields",
+    "deny_unknown_fields",
     "tag",
     "content",
     "untagged",
diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs
index e6b8728..ec1a5a5 100644
--- a/schemars_derive/src/schema_exprs.rs
+++ b/schemars_derive/src/schema_exprs.rs
@@ -17,6 +17,58 @@ pub fn expr_for_container(cont: &Container) -> TokenStream {
     doc_metadata.apply_to_schema(schema_expr)
 }
 
+fn expr_for_field(field: &Field, allow_ref: bool) -> TokenStream {
+    let (ty, type_def) = type_for_schema(field, 0);
+    let span = field.original.span();
+
+    if allow_ref {
+        quote_spanned! {span=>
+            {
+                #type_def
+                gen.subschema_for::<#ty>()
+            }
+        }
+    } else {
+        quote_spanned! {span=>
+            {
+                #type_def
+                <#ty as schemars::JsonSchema>::json_schema(gen)
+            }
+        }
+    }
+}
+
+fn type_for_schema(field: &Field, local_id: usize) -> (syn::Type, Option<TokenStream>) {
+    match &field.attrs.with {
+        None => (field.ty.to_owned(), None),
+        Some(WithAttr::Type(ty)) => (ty.to_owned(), None),
+        Some(WithAttr::Function(fun)) => {
+            let ty_name = format_ident!("_SchemarsSchemaWithFunction{}", local_id);
+            let fn_name = fun.segments.last().unwrap().ident.to_string();
+
+            let type_def = quote_spanned! {fun.span()=>
+                struct #ty_name;
+
+                impl schemars::JsonSchema for #ty_name {
+                    fn is_referenceable() -> bool {
+                        false
+                    }
+
+                    fn schema_name() -> std::string::String {
+                        #fn_name.to_string()
+                    }
+
+                    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+                        #fun(gen)
+                    }
+                }
+            };
+
+            (parse_quote!(#ty_name), Some(type_def))
+        }
+    }
+}
+
 fn expr_for_enum(variants: &[Variant], cattrs: &serde_attr::Container) -> TokenStream {
     let variants = variants
         .iter()
@@ -37,6 +89,7 @@ fn expr_for_external_tagged_enum<'a>(
 
     let unit_names = unit_variants.iter().map(|v| v.name());
     let unit_schema = schema_object(quote! {
+        instance_type: Some(schemars::schema::InstanceType::String.into()),
         enum_values: Some(vec![#(#unit_names.into()),*]),
     });
 
@@ -208,7 +261,7 @@ fn expr_for_untagged_enum_variant(variant: &Variant) -> TokenStream {
 
     match variant.style {
         Style::Unit => expr_for_unit_struct(),
-        Style::Newtype => expr_for_newtype_struct(&variant.fields[0]),
+        Style::Newtype => expr_for_field(&variant.fields[0], true),
         Style::Tuple => expr_for_tuple_struct(&variant.fields),
         Style::Struct => expr_for_struct(&variant.fields, None),
     }
@@ -223,13 +276,7 @@ fn expr_for_untagged_enum_variant_for_flatten(variant: &Variant) -> Option<Token
 
     Some(match variant.style {
         Style::Unit => return None,
-        Style::Newtype => {
-            let field = &variant.fields[0];
-            let ty = field.type_for_schema();
-            quote_spanned! {field.original.span()=>
-                <#ty>::json_schema(gen)
-            }
-        }
+        Style::Newtype => expr_for_field(&variant.fields[0], false),
         Style::Tuple => expr_for_tuple_struct(&variant.fields),
         Style::Struct => expr_for_struct(&variant.fields, None),
     })
@@ -242,19 +289,21 @@ fn expr_for_unit_struct() -> TokenStream {
 }
 
 fn expr_for_newtype_struct(field: &Field) -> TokenStream {
-    let ty = field.type_for_schema();
-    quote_spanned! {field.original.span()=>
-        gen.subschema_for::<#ty>()
-    }
+    expr_for_field(field, true)
 }
 
 fn expr_for_tuple_struct(fields: &[Field]) -> TokenStream {
-    let types = fields
+    let (types, type_defs): (Vec<_>, Vec<_>) = fields
         .iter()
         .filter(|f| !f.serde_attrs.skip_deserializing())
-        .map(Field::type_for_schema);
+        .enumerate()
+        .map(|(i, f)| type_for_schema(f, i))
+        .unzip();
     quote! {
-        gen.subschema_for::<(#(#types),*)>()
+        {
+            #(#type_defs)*
+            gen.subschema_for::<(#(#types),*)>()
+        }
     }
 }
 
@@ -270,7 +319,9 @@ fn expr_for_struct(fields: &[Field], cattrs: Option<&serde_attr::Container>) ->
         SerdeDefault::Path(path) => Some(quote!(let container_default = #path();)),
     });
 
-    let properties = property_fields.iter().map(|field| {
+    let mut type_defs = Vec::new();
+
+    let properties: Vec<_> = property_fields.into_iter().map(|field| {
         let name = field.name();
         let default = field_default_expr(field, set_container_default.is_some());
 
@@ -286,28 +337,48 @@ fn expr_for_struct(fields: &[Field], cattrs: Option<&serde_attr::Container>) ->
             ..SchemaMetadata::from_doc_attrs(&field.original.attrs)
         };
 
-        let ty = field.type_for_schema();
-        let span = field.original.span();
+        let (ty, type_def) = type_for_schema(field, type_defs.len());
+        if let Some(type_def) = type_def {
+            type_defs.push(type_def);
+        }
 
-        quote_spanned! {span=>
-            <#ty>::add_schema_as_property(gen, &mut schema_object, #name.to_owned(), #metadata, #required);
+        quote_spanned! {ty.span()=>
+            <#ty as schemars::JsonSchema>::add_schema_as_property(gen, &mut schema_object, #name.to_owned(), #metadata, #required);
         }
-    });
 
-    let flattens = flattened_fields.iter().map(|field| {
-        let ty = field.type_for_schema();
-        let span = field.original.span();
+    }).collect();
 
-        quote_spanned! {span=>
-            .flatten(<#ty>::json_schema_for_flatten(gen))
-        }
-    });
+    let flattens: Vec<_> = flattened_fields
+        .into_iter()
+        .map(|field| {
+            let (ty, type_def) = type_for_schema(field, type_defs.len());
+            if let Some(type_def) = type_def {
+                type_defs.push(type_def);
+            }
+
+            quote_spanned! {ty.span()=>
+                .flatten(<#ty as schemars::JsonSchema>::json_schema_for_flatten(gen))
+            }
+        })
+        .collect();
+
+    let deny_unknown_fields = cattrs
+        .map_or(false, |attrs| attrs.deny_unknown_fields());
 
     quote! {
         {
+            #(#type_defs)*
             #set_container_default
             let mut schema_object = schemars::schema::SchemaObject {
                 instance_type: Some(schemars::schema::InstanceType::Object.into()),
+                object: Some(Box::new(schemars::schema::ObjectValidation {
+                    additional_properties: if #deny_unknown_fields {
+                        Some(Box::new(false.into()))
+                    } else {
+                        None
+                    },
+                    ..Default::default()
+                })),
                 ..Default::default()
             };
             #(#properties)*


Test patch:
diff --git a/schemars/tests/enum.rs b/schemars/tests/enum.rs
index 774082a..ea34d06 100644
--- a/schemars/tests/enum.rs
+++ b/schemars/tests/enum.rs
@@ -2,6 +2,9 @@ mod util;
 use schemars::{JsonSchema, Map};
 use util::*;
 
+// Ensure that schemars_derive uses the full path to std::string::String
+pub struct String;
+
 #[derive(Debug, JsonSchema)]
 pub struct UnitStruct;
 
@@ -15,7 +18,7 @@ pub struct Struct {
 #[schemars(rename_all = "camelCase")]
 pub enum External {
     UnitOne,
-    StringMap(Map<String, String>),
+    StringMap(Map<&'static str, &'static str>),
     UnitStructNewType(UnitStruct),
     StructNewType(Struct),
     Struct {
@@ -37,7 +40,7 @@ fn enum_external_tag() -> TestResult {
 #[schemars(tag = "typeProperty")]
 pub enum Internal {
     UnitOne,
-    StringMap(Map<String, String>),
+    StringMap(Map<&'static str, &'static str>),
     UnitStructNewType(UnitStruct),
     StructNewType(Struct),
     Struct {
@@ -58,7 +61,7 @@ fn enum_internal_tag() -> TestResult {
 #[schemars(untagged)]
 pub enum Untagged {
     UnitOne,
-    StringMap(Map<String, String>),
+    StringMap(Map<&'static str, &'static str>),
     UnitStructNewType(UnitStruct),
     StructNewType(Struct),
     Struct {
@@ -79,7 +82,7 @@ fn enum_untagged() -> TestResult {
 #[schemars(tag = "t", content = "c")]
 pub enum Adjacent {
     UnitOne,
-    StringMap(Map<String, String>),
+    StringMap(Map<&'static str, &'static str>),
     UnitStructNewType(UnitStruct),
     StructNewType(Struct),
     Struct {
diff --git a/schemars/tests/expected/doc_comments_enum.json b/schemars/tests/expected/doc_comments_enum.json
index 2115c37..1961955 100644
--- a/schemars/tests/expected/doc_comments_enum.json
+++ b/schemars/tests/expected/doc_comments_enum.json
@@ -4,6 +4,7 @@
   "description": "This is the enum's description.",
   "anyOf": [
     {
+      "type": "string",
       "enum": [
         "UndocumentedUnit",
         "DocumentedUnit"
diff --git a/schemars/tests/expected/enum-external.json b/schemars/tests/expected/enum-external.json
index a041210..466d458 100644
--- a/schemars/tests/expected/enum-external.json
+++ b/schemars/tests/expected/enum-external.json
@@ -3,6 +3,7 @@
   "title": "External",
   "anyOf": [
     {
+      "type": "string",
       "enum": [
         "unitOne",
         "unitTwo"
diff --git a/schemars/tests/expected/schema-2019_09.json b/schemars/tests/expected/schema-2019_09.json
index 5592b53..2f7a1ca 100644
--- a/schemars/tests/expected/schema-2019_09.json
+++ b/schemars/tests/expected/schema-2019_09.json
@@ -358,6 +358,7 @@
   "definitions": {
     "InstanceType": {
       "description": "The possible types of values in JSON Schema documents.\n\nSee [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).",
+      "type": "string",
       "enum": [
         "null",
         "boolean",
diff --git a/schemars/tests/expected/schema-openapi3.json b/schemars/tests/expected/schema-openapi3.json
index 6da10c6..8d35dbb 100644
--- a/schemars/tests/expected/schema-openapi3.json
+++ b/schemars/tests/expected/schema-openapi3.json
@@ -294,6 +294,7 @@
   "definitions": {
     "InstanceType": {
       "description": "The possible types of values in JSON Schema documents.\n\nSee [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).",
+      "type": "string",
       "enum": [
         "null",
         "boolean",
diff --git a/schemars/tests/expected/schema.json b/schemars/tests/expected/schema.json
index 8b0d4e0..cef9425 100644
--- a/schemars/tests/expected/schema.json
+++ b/schemars/tests/expected/schema.json
@@ -358,6 +358,7 @@
   "definitions": {
     "InstanceType": {
       "description": "The possible types of values in JSON Schema documents.\n\nSee [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).",
+      "type": "string",
       "enum": [
         "null",
         "boolean",
diff --git a/schemars/tests/expected/schema_with-enum-adjacent-tagged.json b/schemars/tests/expected/schema_with-enum-adjacent-tagged.json
new file mode 100644
index 0000000..3ff17f7
--- /dev/null
+++ b/schemars/tests/expected/schema_with-enum-adjacent-tagged.json
@@ -0,0 +1,79 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Adjacent",
+  "anyOf": [
+    {
+      "type": "object",
+      "required": [
+        "c",
+        "t"
+      ],
+      "properties": {
+        "c": {
+          "type": "object",
+          "required": [
+            "foo"
+          ],
+          "properties": {
+            "foo": {
+              "type": "boolean"
+            }
+          }
+        },
+        "t": {
+          "type": "string",
+          "enum": [
+            "Struct"
+          ]
+        }
+      }
+    },
+    {
+      "type": "object",
+      "required": [
+        "c",
+        "t"
+      ],
+      "properties": {
+        "c": {
+          "type": "boolean"
+        },
+        "t": {
+          "type": "string",
+          "enum": [
+            "NewType"
+          ]
+        }
+      }
+    },
+    {
+      "type": "object",
+      "required": [
+        "c",
+        "t"
+      ],
+      "properties": {
+        "c": {
+          "type": "array",
+          "items": [
+            {
+              "type": "boolean"
+            },
+            {
+              "type": "integer",
+              "format": "int32"
+            }
+          ],
+          "maxItems": 2,
+          "minItems": 2
+        },
+        "t": {
+          "type": "string",
+          "enum": [
+            "Tuple"
+          ]
+        }
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/schemars/tests/expected/schema_with-enum-external.json b/schemars/tests/expected/schema_with-enum-external.json
new file mode 100644
index 0000000..2e7d415
--- /dev/null
+++ b/schemars/tests/expected/schema_with-enum-external.json
@@ -0,0 +1,58 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "External",
+  "anyOf": [
+    {
+      "type": "object",
+      "required": [
+        "struct"
+      ],
+      "properties": {
+        "struct": {
+          "type": "object",
+          "required": [
+            "foo"
+          ],
+          "properties": {
+            "foo": {
+              "type": "boolean"
+            }
+          }
+        }
+      }
+    },
+    {
+      "type": "object",
+      "required": [
+        "newType"
+      ],
+      "properties": {
+        "newType": {
+          "type": "boolean"
+        }
+      }
+    },
+    {
+      "type": "object",
+      "required": [
+        "tuple"
+      ],
+      "properties": {
+        "tuple": {
+          "type": "array",
+          "items": [
+            {
+              "type": "boolean"
+            },
+            {
+              "type": "integer",
+              "format": "int32"
+            }
+          ],
+          "maxItems": 2,
+          "minItems": 2
+        }
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/schemars/tests/expected/schema_with-enum-internal.json b/schemars/tests/expected/schema_with-enum-internal.json
new file mode 100644
index 0000000..2289ef7
--- /dev/null
+++ b/schemars/tests/expected/schema_with-enum-internal.json
@@ -0,0 +1,41 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Internal",
+  "anyOf": [
+    {
+      "type": "object",
+      "required": [
+        "foo",
+        "typeProperty"
+      ],
+      "properties": {
+        "foo": {
+          "type": "boolean"
+        },
+        "typeProperty": {
+          "type": "string",
+          "enum": [
+            "Struct"
+          ]
+        }
+      }
+    },
+    {
+      "type": [
+        "boolean",
+        "object"
+      ],
+      "required": [
+        "typeProperty"
+      ],
+      "properties": {
+        "typeProperty": {
+          "type": "string",
+          "enum": [
+            "NewType"
+          ]
+        }
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/schemars/tests/expected/schema_with-enum-untagged.json b/schemars/tests/expected/schema_with-enum-untagged.json
new file mode 100644
index 0000000..d22f5a3
--- /dev/null
+++ b/schemars/tests/expected/schema_with-enum-untagged.json
@@ -0,0 +1,34 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Untagged",
+  "anyOf": [
+    {
+      "type": "object",
+      "required": [
+        "foo"
+      ],
+      "properties": {
+        "foo": {
+          "type": "boolean"
+        }
+      }
+    },
+    {
+      "type": "boolean"
+    },
+    {
+      "type": "array",
+      "items": [
+        {
+          "type": "boolean"
+        },
+        {
+          "type": "integer",
+          "format": "int32"
+        }
+      ],
+      "maxItems": 2,
+      "minItems": 2
+    }
+  ]
+}
\ No newline at end of file
diff --git a/schemars/tests/expected/schema_with-newtype.json b/schemars/tests/expected/schema_with-newtype.json
new file mode 100644
index 0000000..8b709fb
--- /dev/null
+++ b/schemars/tests/expected/schema_with-newtype.json
@@ -0,0 +1,5 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Newtype",
+  "type": "boolean"
+}
\ No newline at end of file
diff --git a/schemars/tests/expected/schema_with-struct.json b/schemars/tests/expected/schema_with-struct.json
new file mode 100644
index 0000000..190fdff
--- /dev/null
+++ b/schemars/tests/expected/schema_with-struct.json
@@ -0,0 +1,22 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Struct",
+  "type": "object",
+  "required": [
+    "bar",
+    "baz",
+    "foo"
+  ],
+  "properties": {
+    "bar": {
+      "type": "integer",
+      "format": "int32"
+    },
+    "baz": {
+      "type": "boolean"
+    },
+    "foo": {
+      "type": "boolean"
+    }
+  }
+}
\ No newline at end of file
diff --git a/schemars/tests/expected/schema_with-tuple.json b/schemars/tests/expected/schema_with-tuple.json
new file mode 100644
index 0000000..1a8f8ca
--- /dev/null
+++ b/schemars/tests/expected/schema_with-tuple.json
@@ -0,0 +1,19 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Tuple",
+  "type": "array",
+  "items": [
+    {
+      "type": "boolean"
+    },
+    {
+      "type": "integer",
+      "format": "int32"
+    },
+    {
+      "type": "boolean"
+    }
+  ],
+  "maxItems": 3,
+  "minItems": 3
+}
\ No newline at end of file
diff --git a/schemars/tests/expected/skip_enum_variants.json b/schemars/tests/expected/skip_enum_variants.json
index 6ce7aca..c347df0 100644
--- a/schemars/tests/expected/skip_enum_variants.json
+++ b/schemars/tests/expected/skip_enum_variants.json
@@ -3,6 +3,7 @@
   "title": "MyEnum",
   "anyOf": [
     {
+      "type": "string",
       "enum": [
         "Included2"
       ]
diff --git a/schemars/tests/expected/struct-normal-additional-properties.json b/schemars/tests/expected/struct-normal-additional-properties.json
new file mode 100644
index 0000000..7788e4a
--- /dev/null
+++ b/schemars/tests/expected/struct-normal-additional-properties.json
@@ -0,0 +1,25 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Struct",
+  "type": "object",
+  "additionalProperties": false,
+  "required": [
+    "bar",
+    "foo"
+  ],
+  "properties": {
+    "bar": {
+      "type": "boolean"
+    },
+    "baz": {
+      "type": [
+        "string",
+        "null"
+      ]
+    },
+    "foo": {
+      "type": "integer",
+      "format": "int32"
+    }
+  }
+}
diff --git a/schemars/tests/schema_with_enum.rs b/schemars/tests/schema_with_enum.rs
new file mode 100644
index 0000000..38cef03
--- /dev/null
+++ b/schemars/tests/schema_with_enum.rs
@@ -0,0 +1,92 @@
+mod util;
+use schemars::JsonSchema;
+use util::*;
+
+// FIXME determine whether schema_with should be allowed on unit variants
+
+fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+    <bool>::json_schema(gen)
+}
+
+#[derive(Debug)]
+pub struct DoesntImplementJsonSchema;
+
+#[derive(Debug, JsonSchema)]
+#[schemars(rename_all = "camelCase")]
+pub enum External {
+    Struct {
+        #[schemars(schema_with = "schema_fn")]
+        foo: DoesntImplementJsonSchema,
+    },
+    NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema),
+    Tuple(
+        #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema,
+        i32,
+    ),
+    // #[schemars(schema_with = "schema_fn")]
+    // Unit,
+}
+
+#[test]
+fn enum_external_tag() -> TestResult {
+    test_default_generated_schema::<External>("schema_with-enum-external")
+}
+
+#[derive(Debug, JsonSchema)]
+#[schemars(tag = "typeProperty")]
+pub enum Internal {
+    Struct {
+        #[schemars(schema_with = "schema_fn")]
+        foo: DoesntImplementJsonSchema,
+    },
+    NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema),
+    // #[schemars(schema_with = "schema_fn")]
+    // Unit,
+}
+
+#[test]
+fn enum_internal_tag() -> TestResult {
+    test_default_generated_schema::<Internal>("schema_with-enum-internal")
+}
+
+#[derive(Debug, JsonSchema)]
+#[schemars(untagged)]
+pub enum Untagged {
+    Struct {
+        #[schemars(schema_with = "schema_fn")]
+        foo: DoesntImplementJsonSchema,
+    },
+    NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema),
+    Tuple(
+        #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema,
+        i32,
+    ),
+    // #[schemars(schema_with = "schema_fn")]
+    // Unit,
+}
+
+#[test]
+fn enum_untagged() -> TestResult {
+    test_default_generated_schema::<Untagged>("schema_with-enum-untagged")
+}
+
+#[derive(Debug, JsonSchema)]
+#[schemars(tag = "t", content = "c")]
+pub enum Adjacent {
+    Struct {
+        #[schemars(schema_with = "schema_fn")]
+        foo: DoesntImplementJsonSchema,
+    },
+    NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema),
+    Tuple(
+        #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema,
+        i32,
+    ),
+    // #[schemars(schema_with = "schema_fn")]
+    // Unit,
+}
+
+#[test]
+fn enum_adjacent_tagged() -> TestResult {
+    test_default_generated_schema::<Adjacent>("schema_with-enum-adjacent-tagged")
+}
diff --git a/schemars/tests/schema_with_struct.rs b/schemars/tests/schema_with_struct.rs
new file mode 100644
index 0000000..1e94149
--- /dev/null
+++ b/schemars/tests/schema_with_struct.rs
@@ -0,0 +1,44 @@
+mod util;
+use schemars::JsonSchema;
+use util::*;
+
+fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+    <bool>::json_schema(gen)
+}
+
+#[derive(Debug)]
+struct DoesntImplementJsonSchema;
+
+#[derive(Debug, JsonSchema)]
+pub struct Struct {
+    #[schemars(schema_with = "schema_fn")]
+    foo: DoesntImplementJsonSchema,
+    bar: i32,
+    #[schemars(schema_with = "schema_fn")]
+    baz: DoesntImplementJsonSchema,
+}
+
+#[test]
+fn struct_normal() -> TestResult {
+    test_default_generated_schema::<Struct>("schema_with-struct")
+}
+
+#[derive(Debug, JsonSchema)]
+pub struct Tuple(
+    #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema,
+    i32,
+    #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema,
+);
+
+#[test]
+fn struct_tuple() -> TestResult {
+    test_default_generated_schema::<Tuple>("schema_with-tuple")
+}
+
+#[derive(Debug, JsonSchema)]
+pub struct Newtype(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema);
+
+#[test]
+fn struct_newtype() -> TestResult {
+    test_default_generated_schema::<Newtype>("schema_with-newtype")
+}
diff --git a/schemars/tests/struct.rs b/schemars/tests/struct.rs
index e522794..73ce851 100644
--- a/schemars/tests/struct.rs
+++ b/schemars/tests/struct.rs
@@ -2,11 +2,14 @@ mod util;
 use schemars::JsonSchema;
 use util::*;
 
+// Ensure that schemars_derive uses the full path to std::string::String
+pub struct String;
+
 #[derive(Debug, JsonSchema)]
 pub struct Struct {
     foo: i32,
     bar: bool,
-    baz: Option<String>,
+    baz: Option<&'static str>,
 }
 
 #[test]
@@ -15,7 +18,7 @@ fn struct_normal() -> TestResult {
 }
 
 #[derive(Debug, JsonSchema)]
-pub struct Tuple(i32, bool, Option<String>);
+pub struct Tuple(i32, bool, Option<&'static str>);
 
 #[test]
 fn struct_tuple() -> TestResult {
diff --git a/schemars/tests/struct_additional_properties.rs b/schemars/tests/struct_additional_properties.rs
new file mode 100644
index 0000000..294e465
--- /dev/null
+++ b/schemars/tests/struct_additional_properties.rs
@@ -0,0 +1,16 @@
+mod util;
+use schemars::JsonSchema;
+use util::*;
+
+#[derive(Debug, JsonSchema)]
+#[serde(deny_unknown_fields)]
+pub struct Struct {
+    foo: i32,
+    bar: bool,
+    baz: Option<String>,
+}
+
+#[test]
+fn struct_normal_additional_properties() -> TestResult {
+    test_default_generated_schema::<Struct>("struct-normal-additional-properties")
+}