Repository: GREsau/schemars
Pull request #36 (2020-05-26 22:11:58)

Adding `IndexMap` under feature flag `preserve_order`
WIP: Do not merge yet
Implementation for #32 

This is only a small change.
Most of the change is to make `impl<K, V> Merge for Map<K, V>` work again.
Because the `Extens<(K, V)>` for `IndexMap<K, V, S>` implementation relays on `K: Hash + Eq + Copy, V: Copy,` As Copy is not always implemented for things like String and most structs. So I make a small implementation of `extend` in there. With `insert` that only needs to relays `K: std::hash::Hash + Eq`. The `Ord` trait was already there. Just moved it down.

## Tested
I tested the code both in the okapi example and my code. Both work with and without the feature flag set.

## Import in code
When testing make sure you point `okapi`,`rocket-okapi` and your own code to the same `Schemars` version.

Example in my code (and okapi example): (Cargo.toml)
```toml
# My normal way of importing schemars
schemars = "0.7"
# With feature flag pointing to my local copy.
schemars = { version = "0.7.0", 
path = "path_to_somewhere/schemars/schemars", features = ["preserve_order"] }
# Without the feature flag.
schemars = { version = "0.7.0", 
path = "path_to_somewhere/schemars/schemars", features = [] }
```

## Weird API call list order
The only odd behavior that I have noticed is that the order of the api calls is not consistent.
Order without feature flag:
![Screenshot from 2020-05-26 23-42-37](https://user-images.githubusercontent.com/2608639/82953510-2f4cae80-9fab-11ea-8b92-409108b6f9d7.png)
Order with flag run first time:
![Screenshot from 2020-05-26 23-47-27](https://user-images.githubusercontent.com/2608639/82953604-586d3f00-9fab-11ea-9a06-8a979f49e22a.png)
Close and run again:
![Screenshot from 2020-05-26 23-47-37](https://user-images.githubusercontent.com/2608639/82953619-5f944d00-9fab-11ea-9ebf-d8f553ef38bb.png)

This is different every time you close and start the server again (does not even have to recompile).
I do not know how this comes. But also have not looked into this yet.
The order in the rocket output is always the same.
I have only seen this with the routes not the other parts.
Order changes in json as you can see here (both with feature flag, just closed and started server in between):
![Screenshot from 2020-05-26 23-55-36](https://user-images.githubusercontent.com/2608639/82954179-80a96d80-9fac-11ea-9268-1789c0f45f08.png)
![Screenshot from 2020-05-26 23-55-58](https://user-images.githubusercontent.com/2608639/82954184-830bc780-9fac-11ea-92f3-24d210382a28.png)

## Works, proof
But here is proof that is works in other cases:
![Without feature flag](https://user-images.githubusercontent.com/2608639/82953469-1fcd6580-9fab-11ea-87f5-5a9ae1e60c5a.png)
With flag:
![Screenshot from 2020-05-26 23-48-52](https://user-images.githubusercontent.com/2608639/82953680-83579300-9fab-11ea-92a8-186d4ca71c8f.png)

## Feedback
So what do you think? What can be changes/improved. You maybe have an idea where the order bug comes from? Maybe just timing or multi-threading?

@ThouCheese are you maybe willing to give this a try in your project to see if you spot any compatibility error?


GREsau: The inconsistent ordering may be caused by this usage of hashmap in okapi:
https://github.com/GREsau/okapi/blob/18a30b7353b8748f23844fbe6c56694b64536773/rocket-okapi/src/gen.rs#L17

...which should probably be an `okapi::Map` (equivalent to `schemars::Map`) instead of a hashmap
ralpha: > The inconsistent ordering may be caused by this usage of hashmap in okapi:
> https://github.com/GREsau/okapi/blob/18a30b7353b8748f23844fbe6c56694b64536773/rocket-okapi/src/gen.rs#L17
> 
> ...which should probably be an `okapi::Map` (equivalent to `schemars::Map`) instead of a hashmap

Some limited testing seems to confirm this.
Re implemented it as Map first. But that does not work because BTreeMap.
`the trait bound `rocket_http::method::Method: std::cmp::Ord` is not satisfied`
So I made a `type AltMap<K, V>` that implements HashMap if flag is not set and IndexMap when flag is set.

Also had to do expose the `Entry` for each `Map`.
See commit.
ralpha: In rocket-okapi changed
```
// Old
use okapi::Map;
// New
use schemars::{Map, AltMap, AltMapEntry};
```
And used `AltMap` and `AltMapEntry` in lines below.
This seems to fix the ordering problem.
Will add documentation later. (When we are getting ready to merge his)
Other remarks?
GREsau: Rather than introducing AltMap, I'd rather change the usage of HashMap (which currently causes okapi to be nondeterministic) to BTreeMap/IndexMap.

This would require making the key implement `Ord` - I think one way of doing this would be to change the key from `(String, Method)` to `(String, &'static str)`, using [as_str()](https://api.rocket.rs/v0.4/rocket/http/enum.Method.html#method.as_str) to convert the method to its string representation.
GREsau: Other options for replacing `HashMap<(String, Method), Operation>` with:
- `schemars::Map<String, HashMap<Method, Operation>>`
- `schemars::Map<String, PathItem>`

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1e0827..4e35cb7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
 # Changelog
 
+## In-dev - version TBC
+### Added:
+- `visit::Visitor`, a trait for updating a schema and all schemas it contains recursively. A `SchemaSettings` can now contain a list of visitors.
+- `into_object()` method added to `Schema` as a shortcut for `into::<SchemaObject>()`
+
+### Removed (**BREAKING CHANGES**):
+- `SchemaSettings::bool_schemas` - this has been superseded by the `ReplaceBoolSchemas` visitor
+- `SchemaSettings::allow_ref_siblings` - this has been superseded by the `RemoveRefSiblings` visitor
+- `SchemaSettings` no longer implements `PartialEq`
+
+### Fixed:
+- **BREAKING CHANGE** unknown items in `#[schemars(...)]` attributes now cause a compilation error (https://github.com/GREsau/schemars/issues/18)
+
+### Deprecated:
+- `make_extensible`, `schema_for_any`, and `schema_for_none` methods on `SchemaGenerator`
+
 ## [0.7.6] - 2020-05-17
 ### Added:
 - `#[schemars(example = "...")]` attribute for setting examples on generated schemas (https://github.com/GREsau/schemars/issues/23)
diff --git a/schemars/Cargo.toml b/schemars/Cargo.toml
index f61e4b6..cb727c9 100644
--- a/schemars/Cargo.toml
+++ b/schemars/Cargo.toml
@@ -16,9 +16,10 @@ build = "build.rs"
 schemars_derive = { version = "=0.7.6", optional = true, path = "../schemars_derive" }
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
+dyn-clone = "1.0"
 
 chrono = { version = "0.4", default-features = false, optional = true }
-indexmap = { version = "1.2", optional = true }
+indexmap = { version = "1.2", features=["serde-1"], optional = true }
 either = { version = "1.3", default-features = false, optional = true }
 uuid = { version = "0.8", default-features = false, optional = true }
 smallvec = { version = "1.0", optional = true }
@@ -26,16 +27,24 @@ arrayvec = { version = "0.5", default-features = false, optional = true }
 
 [dev-dependencies]
 pretty_assertions = "0.6.1"
+trybuild = "1.0"
 
 [features]
 default = ["derive"]
 
 derive = ["schemars_derive"]
 
+# Use a different representation for the map type of Schemars.
+# This allows data to be read into a Value and written back to a JSON string
+# while preserving the order of map keys in the input.
+preserve_order = ["indexmap"]
+
 impl_json_schema = ["derive"]
 # derive_json_schema will be removed in a later version
 derive_json_schema = ["impl_json_schema"]
 
+ui_test = []
+
 [[test]]
 name = "chrono"
 required-features = ["chrono"]
@@ -64,5 +73,9 @@ required-features = ["arrayvec"]
 name = "schema_for_schema"
 required-features = ["impl_json_schema"]
 
+[[test]]
+name = "ui"
+required-features = ["ui_test"]
+
 [package.metadata.docs.rs]
 all-features = true
diff --git a/schemars/src/flatten.rs b/schemars/src/flatten.rs
index d3c777e..1e7dab3 100644
--- a/schemars/src/flatten.rs
+++ b/schemars/src/flatten.rs
@@ -101,7 +101,10 @@ impl<T> Merge for Vec<T> {
     }
 }
 
-impl<K: Ord, V> Merge for Map<K, V> {
+impl<K, V> Merge for Map<K, V>
+where
+    K: std::hash::Hash + Eq + Ord,
+{
     fn merge(mut self, other: Self) -> Self {
         self.extend(other);
         self
diff --git a/schemars/src/gen.rs b/schemars/src/gen.rs
index 81a9c7a..a107e77 100644
--- a/schemars/src/gen.rs
+++ b/schemars/src/gen.rs
@@ -1,12 +1,14 @@
 use crate::flatten::Merge;
 use crate::schema::*;
-use crate::{JsonSchema, Map};
+use crate::{visit::*, JsonSchema, Map};
+use dyn_clone::DynClone;
+use std::{any::Any, fmt::Debug};
 
 /// Settings to customize how Schemas are generated.
 ///
 /// The default settings currently conform to [JSON Schema Draft 7](https://json-schema.org/specification-links.html#draft-7), but this is liable to change in a future version of Schemars if support for other JSON Schema versions is added.
 /// If you require your generated schemas to conform to draft 7, consider using the [`draft07`](#method.draft07) method.
-#[derive(Debug, PartialEq, Clone)]
+#[derive(Debug, Clone)]
 pub struct SchemaSettings {
     /// If `true`, schemas for [`Option<T>`](Option) will include a `nullable` property.
     ///
@@ -18,10 +20,6 @@ pub struct SchemaSettings {
     ///
     /// Defaults to `true`.
     pub option_add_null_type: bool,
-    /// Controls whether trivial [`Bool`](../schema/enum.Schema.html#variant.Bool) schemas may be generated.
-    ///
-    /// Defaults to [`BoolSchemas::Enabled`].
-    pub bool_schemas: BoolSchemas,
     /// A JSON pointer to the expected location of referenceable subschemas within the resulting root schema.
     ///
     /// Defaults to `"#/definitions/"`.
@@ -30,24 +28,11 @@ pub struct SchemaSettings {
     ///
     /// Defaults to `"http://json-schema.org/draft-07/schema#"`.
     pub meta_schema: Option<String>,
-    /// Whether schemas with a `$ref` property may have other properties set.
-    ///
-    /// Defaults to `false`.
-    pub allow_ref_siblings: bool,
+    /// A list of visitors that get applied to all generated root schemas.
+    pub visitors: Vec<Box<dyn Visitor2>>,
     _hidden: (),
 }
 
-/// Controls whether trivial [`Bool`](../schema/enum.Schema.html#variant.Bool) schemas may be generated.
-#[derive(Debug, PartialEq, Copy, Clone)]
-pub enum BoolSchemas {
-    /// `Bool` schemas may be used.
-    Enabled,
-    /// `Bool` schemas may only be used in a schema's [`additionalProperties`](../schema/struct.ObjectValidation.html#structfield.additional_properties) field.
-    AdditionalPropertiesOnly,
-    /// `Bool` schemas will never be used.
-    Disabled,
-}
-
 impl Default for SchemaSettings {
     fn default() -> SchemaSettings {
         SchemaSettings::draft07()
@@ -60,10 +45,9 @@ impl SchemaSettings {
         SchemaSettings {
             option_nullable: false,
             option_add_null_type: true,
-            bool_schemas: BoolSchemas::Enabled,
             definitions_path: "#/definitions/".to_owned(),
             meta_schema: Some("http://json-schema.org/draft-07/schema#".to_owned()),
-            allow_ref_siblings: false,
+            visitors: vec![Box::new(RemoveRefSiblings)],
             _hidden: (),
         }
     }
@@ -73,10 +57,9 @@ impl SchemaSettings {
         SchemaSettings {
             option_nullable: false,
             option_add_null_type: true,
-            bool_schemas: BoolSchemas::Enabled,
             definitions_path: "#/definitions/".to_owned(),
             meta_schema: Some("https://json-schema.org/draft/2019-09/schema".to_owned()),
-            allow_ref_siblings: true,
+            visitors: Vec::default(),
             _hidden: (),
         }
     }
@@ -86,13 +69,17 @@ impl SchemaSettings {
         SchemaSettings {
             option_nullable: true,
             option_add_null_type: false,
-            bool_schemas: BoolSchemas::AdditionalPropertiesOnly,
             definitions_path: "#/components/schemas/".to_owned(),
             meta_schema: Some(
                 "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema"
                     .to_owned(),
             ),
-            allow_ref_siblings: false,
+            visitors: vec![
+                Box::new(RemoveRefSiblings),
+                Box::new(ReplaceBoolSchemas {
+                    skip_additional_properties: true,
+                }),
+            ],
             _hidden: (),
         }
     }
@@ -114,6 +101,12 @@ impl SchemaSettings {
         self
     }
 
+    /// Appends the given visitor to the list of [visitors](SchemaSettings::visitors) for these `SchemaSettings`.
+    pub fn with_visitor(mut self, visitor: impl Visitor + Debug + DynClone + 'static) -> Self {
+        self.visitors.push(Box::new(visitor));
+        self
+    }
+
     /// Creates a new [`SchemaGenerator`] using these settings.
     pub fn into_generator(self) -> SchemaGenerator {
         SchemaGenerator::new(self)
@@ -170,59 +163,17 @@ impl SchemaGenerator {
         &self.settings
     }
 
-    /// Modifies the given `SchemaObject` so that it may have validation, metadata or other properties set on it.
-    ///
-    /// If `schema` is not a `$ref` schema, then this does not modify `schema`. Otherwise, depending on this generator's settings,
-    /// this may wrap the `$ref` in another schema. This is required because in many JSON Schema implementations, a schema with `$ref`
-    /// set may not include other properties.
-    ///
-    /// # Example
-    /// ```
-    /// use schemars::{gen::SchemaGenerator, schema::SchemaObject};
-    ///
-    /// let gen = SchemaGenerator::default();
-    ///
-    /// let ref_schema = SchemaObject::new_ref("foo".to_owned());
-    /// assert!(ref_schema.is_ref());
-    ///
-    /// let mut extensible_schema = ref_schema.clone();
-    /// gen.make_extensible(&mut extensible_schema);
-    /// assert_ne!(ref_schema, extensible_schema);
-    /// assert!(!extensible_schema.is_ref());
-    ///
-    /// let mut extensible_schema2 = extensible_schema.clone();
-    /// gen.make_extensible(&mut extensible_schema);
-    /// assert_eq!(extensible_schema, extensible_schema2);
-    /// ```
-    pub fn make_extensible(&self, schema: &mut SchemaObject) {
-        if schema.is_ref() && !self.settings().allow_ref_siblings {
-            let original = std::mem::replace(schema, SchemaObject::default());
-            schema.subschemas().all_of = Some(vec![original.into()]);
-        }
-    }
+    #[deprecated = "This method no longer has any effect."]
+    pub fn make_extensible(&self, _schema: &mut SchemaObject) {}
 
-    /// Returns a `Schema` that matches everything, such as the empty schema `{}`.
-    ///
-    /// The exact value returned depends on this generator's [`BoolSchemas`](struct.SchemaSettings.html#structfield.bool_schemas) setting.
+    #[deprecated = "Use `Schema::Bool(true)` instead"]
     pub fn schema_for_any(&self) -> Schema {
-        let schema: Schema = true.into();
-        if self.settings().bool_schemas == BoolSchemas::Enabled {
-            schema
-        } else {
-            Schema::Object(schema.into())
-        }
+        Schema::Bool(true)
     }
 
-    /// Returns a `Schema` that matches nothing, such as the schema `{ "not":{} }`.
-    ///
-    /// The exact value returned depends on this generator's [`BoolSchemas`](struct.SchemaSettings.html#structfield.bool_schemas) setting.
+    #[deprecated = "Use `Schema::Bool(false)` instead"]
     pub fn schema_for_none(&self) -> Schema {
-        let schema: Schema = false.into();
-        if self.settings().bool_schemas == BoolSchemas::Enabled {
-            schema
-        } else {
-            Schema::Object(schema.into())
-        }
+        Schema::Bool(false)
     }
 
     /// Generates a JSON Schema for the type `T`, and returns either the schema itself or a `$ref` schema referencing `T`'s schema.
@@ -253,7 +204,7 @@ impl SchemaGenerator {
         self.definitions.insert(name, schema);
     }
 
-    /// Returns the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated.
+    /// Borrows the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated.
     ///
     /// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the values are the schemas
     /// themselves.
@@ -275,14 +226,19 @@ impl SchemaGenerator {
     /// add them to the `SchemaGenerator`'s schema definitions and include them in the returned `SchemaObject`'s
     /// [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
     pub fn root_schema_for<T: ?Sized + JsonSchema>(&mut self) -> RootSchema {
-        let mut schema = T::json_schema(self).into();
-        self.make_extensible(&mut schema);
+        let mut schema = T::json_schema(self).into_object();
         schema.metadata().title.get_or_insert_with(T::schema_name);
-        RootSchema {
+        let mut root = RootSchema {
             meta_schema: self.settings.meta_schema.clone(),
             definitions: self.definitions.clone(),
             schema,
+        };
+
+        for visitor in &mut self.settings.visitors {
+            visitor.visit_root_schema(&mut root)
         }
+
+        root
     }
 
     /// Consumes `self` and generates a root JSON Schema for the type `T`.
@@ -290,14 +246,19 @@ impl SchemaGenerator {
     /// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will
     /// include them in the returned `SchemaObject`'s [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
     pub fn into_root_schema_for<T: ?Sized + JsonSchema>(mut self) -> RootSchema {
-        let mut schema = T::json_schema(&mut self).into();
-        self.make_extensible(&mut schema);
+        let mut schema = T::json_schema(&mut self).into_object();
         schema.metadata().title.get_or_insert_with(T::schema_name);
-        RootSchema {
+        let mut root = RootSchema {
             meta_schema: self.settings.meta_schema,
             definitions: self.definitions,
             schema,
+        };
+
+        for visitor in &mut self.settings.visitors {
+            visitor.visit_root_schema(&mut root)
         }
+
+        root
     }
 
     /// Attemps to find the schema that the given `schema` is referencing.
@@ -352,13 +313,47 @@ impl SchemaGenerator {
             None => return schema,
             Some(ref metadata) if *metadata == Metadata::default() => return schema,
             Some(metadata) => {
-                let mut schema_obj = schema.into();
-
-                self.make_extensible(&mut schema_obj);
+                let mut schema_obj = schema.into_object();
                 schema_obj.metadata = Some(Box::new(metadata)).merge(schema_obj.metadata);
-
                 Schema::Object(schema_obj)
             }
         }
     }
 }
+
+/// A [Visitor](Visitor) which implements additional traits required to be included in a [SchemaSettings].
+///
+/// You will rarely need to use this trait directly as it is automatically implemented for any type which implements all of:
+/// - [`Visitor`]
+/// - [`std::fmt::Debug`]
+/// - [`std::any::Any`] (implemented for all `'static` types)
+/// - [`dyn_clone::DynClone`] (or [`std::clone::Clone`])
+///
+/// # Example
+/// ```
+/// use schemars::visit::Visitor;
+/// use schemars::gen::Visitor2;
+///
+/// #[derive(Debug, Clone)]
+/// struct MyVisitor;
+///
+/// impl Visitor for MyVisitor { }
+///
+/// let v: &dyn Visitor2 = &MyVisitor;
+/// assert_eq!(v.as_any().is::<MyVisitor>(), true);
+/// ```
+pub trait Visitor2: Visitor + Debug + DynClone + Any {
+    /// Upcasts this visitor into an `Any`, which can be used to inspect and manipulate it as its concrete type.
+    fn as_any(&self) -> &dyn Any;
+}
+
+dyn_clone::clone_trait_object!(Visitor2);
+
+impl<T> Visitor2 for T
+where
+    T: Visitor + Debug + DynClone + Any,
+{
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
diff --git a/schemars/src/json_schema_impls/core.rs b/schemars/src/json_schema_impls/core.rs
index 84917c5..2c5b4b6 100644
--- a/schemars/src/json_schema_impls/core.rs
+++ b/schemars/src/json_schema_impls/core.rs
@@ -35,8 +35,7 @@ impl<T: JsonSchema> JsonSchema for Option<T> {
             }
         }
         if gen.settings().option_nullable {
-            let mut schema_obj = schema.into();
-            gen.make_extensible(&mut schema_obj);
+            let mut schema_obj = schema.into_object();
             schema_obj
                 .extensions
                 .insert("nullable".to_owned(), json!(true));
@@ -178,8 +177,7 @@ forward_impl!((<'a> JsonSchema for std::fmt::Arguments<'a>) => String);
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::gen::*;
-    use crate::tests::{custom_schema_object_for, schema_for, schema_object_for};
+    use crate::tests::{schema_for, schema_object_for};
     use pretty_assertions::assert_eq;
 
     #[test]
@@ -209,21 +207,6 @@ mod tests {
         assert_eq!(any_of[1], schema_for::<()>());
     }
 
-    #[test]
-    fn schema_for_option_with_nullable() {
-        let settings = SchemaSettings::default().with(|s| {
-            s.option_nullable = true;
-            s.option_add_null_type = false;
-        });
-        let schema = custom_schema_object_for::<Option<i32>>(settings);
-        assert_eq!(
-            schema.instance_type,
-            Some(SingleOrVec::from(InstanceType::Integer))
-        );
-        assert_eq!(schema.extensions.get("nullable"), Some(&json!(true)));
-        assert_eq!(schema.subschemas.is_none(), true);
-    }
-
     #[test]
     fn schema_for_result() {
         let schema = schema_object_for::<Result<bool, String>>();
diff --git a/schemars/src/json_schema_impls/maps.rs b/schemars/src/json_schema_impls/maps.rs
index f3f08fa..356464f 100644
--- a/schemars/src/json_schema_impls/maps.rs
+++ b/schemars/src/json_schema_impls/maps.rs
@@ -1,4 +1,4 @@
-use crate::gen::{BoolSchemas, SchemaGenerator};
+use crate::gen::SchemaGenerator;
 use crate::schema::*;
 use crate::JsonSchema;
 
@@ -16,18 +16,10 @@ macro_rules! map_impl {
 
             fn json_schema(gen: &mut SchemaGenerator) -> Schema {
                 let subschema = gen.subschema_for::<V>();
-                let json_schema_bool = gen.settings().bool_schemas == BoolSchemas::AdditionalPropertiesOnly
-                    && subschema == gen.schema_for_any();
-                let additional_properties =
-                    if json_schema_bool {
-                        true.into()
-                    } else {
-                        subschema.into()
-                    };
                 SchemaObject {
                     instance_type: Some(InstanceType::Object.into()),
                     object: Some(Box::new(ObjectValidation {
-                        additional_properties: Some(Box::new(additional_properties)),
+                        additional_properties: Some(Box::new(subschema)),
                         ..Default::default()
                     })),
                     ..Default::default()
@@ -40,68 +32,3 @@ macro_rules! map_impl {
 
 map_impl!(<K, V> JsonSchema for std::collections::BTreeMap<K, V>);
 map_impl!(<K, V, H> JsonSchema for std::collections::HashMap<K, V, H>);
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::gen::*;
-    use crate::tests::{custom_schema_object_for, schema_for};
-    use pretty_assertions::assert_eq;
-    use std::collections::BTreeMap;
-
-    #[test]
-    fn schema_for_map_any_value() {
-        for bool_schemas in &[BoolSchemas::Enabled, BoolSchemas::AdditionalPropertiesOnly] {
-            let settings = SchemaSettings::default().with(|s| s.bool_schemas = *bool_schemas);
-            let schema = custom_schema_object_for::<BTreeMap<String, serde_json::Value>>(settings);
-            assert_eq!(
-                schema.instance_type,
-                Some(SingleOrVec::from(InstanceType::Object))
-            );
-            let additional_properties = schema
-                .object
-                .unwrap()
-                .additional_properties
-                .expect("additionalProperties field present");
-            assert_eq!(*additional_properties, Schema::Bool(true));
-        }
-    }
-
-    #[test]
-    fn schema_for_map_any_value_no_bool_schema() {
-        let settings = SchemaSettings::default().with(|s| s.bool_schemas = BoolSchemas::Disabled);
-        let schema = custom_schema_object_for::<BTreeMap<String, serde_json::Value>>(settings);
-        assert_eq!(
-            schema.instance_type,
-            Some(SingleOrVec::from(InstanceType::Object))
-        );
-        let additional_properties = schema
-            .object
-            .unwrap()
-            .additional_properties
-            .expect("additionalProperties field present");
-        assert_eq!(*additional_properties, Schema::Object(Default::default()));
-    }
-
-    #[test]
-    fn schema_for_map_int_value() {
-        for bool_schemas in &[
-            BoolSchemas::Enabled,
-            BoolSchemas::Disabled,
-            BoolSchemas::AdditionalPropertiesOnly,
-        ] {
-            let settings = SchemaSettings::default().with(|s| s.bool_schemas = *bool_schemas);
-            let schema = custom_schema_object_for::<BTreeMap<String, i32>>(settings);
-            assert_eq!(
-                schema.instance_type,
-                Some(SingleOrVec::from(InstanceType::Object))
-            );
-            let additional_properties = schema
-                .object
-                .unwrap()
-                .additional_properties
-                .expect("additionalProperties field present");
-            assert_eq!(*additional_properties, schema_for::<i32>());
-        }
-    }
-}
diff --git a/schemars/src/json_schema_impls/serdejson.rs b/schemars/src/json_schema_impls/serdejson.rs
index 17f714c..d0cceff 100644
--- a/schemars/src/json_schema_impls/serdejson.rs
+++ b/schemars/src/json_schema_impls/serdejson.rs
@@ -11,8 +11,8 @@ impl JsonSchema for Value {
         "AnyValue".to_owned()
     }
 
-    fn json_schema(gen: &mut SchemaGenerator) -> Schema {
-        gen.schema_for_any()
+    fn json_schema(_: &mut SchemaGenerator) -> Schema {
+        Schema::Bool(true)
     }
 }
 
diff --git a/schemars/src/lib.rs b/schemars/src/lib.rs
index 0aad758..cd2e068 100644
--- a/schemars/src/lib.rs
+++ b/schemars/src/lib.rs
@@ -197,8 +197,9 @@ fn main() {
 `#[serde(...)]` attributes can be overriden using `#[schemars(...)]` attributes, which behave identically (e.g. `#[schemars(rename_all = "camelCase")]`). You may find this useful if you want to change the generated schema without affecting Serde's behaviour, or if you're just not using Serde.
 
 ## Feature Flags
-- `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro
-- `impl_json_schema` - implements `JsonSchema` for Schemars types themselves
+- `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro.
+- `impl_json_schema` - implements `JsonSchema` for Schemars types themselves.
+- `preserve_order` - keep the order of structure fields in `Schema` and `SchemaObject`.
 
 ## Optional Dependencies
 Schemars can implement `JsonSchema` on types from several popular crates, enabled via optional dependencies (dependency versions are shown in brackets):
@@ -212,15 +213,27 @@ Schemars can implement `JsonSchema` on types from several popular crates, enable
 
 /// The map type used by schemars types.
 ///
-/// Currently a `BTreeMap`, but this may change a different implementation
+/// Currently a `BTreeMap` or `IndexMap` can be used, but this may change to a different implementation
 /// with a similar interface in a future version of schemars.
+/// The `IndexMap` will be used when the `preserve_order` feature flag is set.
+#[cfg(not(feature = "preserve_order"))]
 pub type Map<K, V> = std::collections::BTreeMap<K, V>;
+#[cfg(feature = "preserve_order")]
+pub type Map<K, V> = indexmap::IndexMap<K, V>;
 /// The set type used by schemars types.
 ///
-/// Currently a `BTreeSet`, but this may change a different implementation
+/// Currently a `BTreeSet`, but this may change to a different implementation
 /// with a similar interface in a future version of schemars.
 pub type Set<T> = std::collections::BTreeSet<T>;
 
+/// A view into a single entry in a map, which may either be vacant or occupied.
+/// This is constructed from the `entry` method on `BTreeMap` or `IndexMap`
+/// depending on whether the `preserve_order` feature flag is set.
+#[cfg(not(feature = "preserve_order"))]
+pub type MapEntry<'a, K, V> = std::collections::btree_map::Entry<'a, K, V>;
+#[cfg(feature = "preserve_order")]
+pub type MapEntry<'a, K, V> = indexmap::map::Entry<'a, K, V>;
+
 mod flatten;
 mod json_schema_impls;
 #[macro_use]
@@ -230,6 +243,8 @@ mod macros;
 pub mod gen;
 /// JSON Schema types.
 pub mod schema;
+/// TODO document
+pub mod visit;
 
 #[cfg(feature = "schemars_derive")]
 extern crate schemars_derive;
@@ -323,18 +338,9 @@ pub mod tests {
         schema_object(schema_for::<T>())
     }
 
-    pub fn custom_schema_object_for<T: JsonSchema>(
-        settings: gen::SchemaSettings,
-    ) -> schema::SchemaObject {
-        schema_object(custom_schema_for::<T>(settings))
-    }
-
     pub fn schema_for<T: JsonSchema>() -> schema::Schema {
-        custom_schema_for::<T>(Default::default())
-    }
-
-    pub fn custom_schema_for<T: JsonSchema>(settings: gen::SchemaSettings) -> schema::Schema {
-        T::json_schema(&mut gen::SchemaGenerator::new(settings))
+        let mut gen = gen::SchemaGenerator::default();
+        T::json_schema(&mut gen)
     }
 
     pub fn schema_object(schema: schema::Schema) -> schema::SchemaObject {
diff --git a/schemars/src/macros.rs b/schemars/src/macros.rs
index 2268826..ba2dc4b 100644
--- a/schemars/src/macros.rs
+++ b/schemars/src/macros.rs
@@ -1,4 +1,4 @@
-/// Generates a [`Schema`](schema::Schema) for the given type using default settings.
+/// Generates a [`RootSchema`](schema::RootSchema) for the given type using default settings.
 ///
 /// The type must implement [`JsonSchema`].
 ///
diff --git a/schemars/src/schema.rs b/schemars/src/schema.rs
index cfbce53..e1b606c 100644
--- a/schemars/src/schema.rs
+++ b/schemars/src/schema.rs
@@ -40,6 +40,32 @@ impl Schema {
             _ => false,
         }
     }
+
+    /// Converts the given schema (if it is a boolean schema) into an equivalent schema object.
+    ///
+    /// If the given schema is already a schema object, this has no effect.
+    ///
+    /// # Example
+    /// ```
+    /// use schemars::schema::{Schema, SchemaObject};
+    ///
+    /// let bool_schema = Schema::Bool(true);
+    ///
+    /// assert_eq!(bool_schema.into_object(), SchemaObject::default());
+    /// ```
+    pub fn into_object(self) -> SchemaObject {
+        match self {
+            Schema::Object(o) => o,
+            Schema::Bool(true) => SchemaObject::default(),
+            Schema::Bool(false) => SchemaObject {
+                subschemas: Some(Box::new(SubschemaValidation {
+                    not: Some(Schema::Object(Default::default()).into()),
+                    ..Default::default()
+                })),
+                ..Default::default()
+            },
+        }
+    }
 }
 
 impl From<SchemaObject> for Schema {
@@ -204,17 +230,7 @@ impl SchemaObject {
 
 impl From<Schema> for SchemaObject {
     fn from(schema: Schema) -> Self {
-        match schema {
-            Schema::Object(o) => o,
-            Schema::Bool(true) => SchemaObject::default(),
-            Schema::Bool(false) => SchemaObject {
-                subschemas: Some(Box::new(SubschemaValidation {
-                    not: Some(Schema::Object(Default::default()).into()),
-                    ..Default::default()
-                })),
-                ..Default::default()
-            },
-        }
+        schema.into_object()
     }
 }
 
diff --git a/schemars/src/visit.rs b/schemars/src/visit.rs
new file mode 100644
index 0000000..b9e3d50
--- /dev/null
+++ b/schemars/src/visit.rs
@@ -0,0 +1,148 @@
+use crate::schema::{RootSchema, Schema, SchemaObject, SingleOrVec};
+use std::fmt::Debug;
+
+/// TODO document
+pub trait Visitor {
+    /// TODO document
+    fn visit_root_schema(&mut self, root: &mut RootSchema) {
+        visit_root_schema(self, root)
+    }
+
+    /// TODO document
+    fn visit_schema(&mut self, schema: &mut Schema) {
+        visit_schema(self, schema)
+    }
+
+    /// TODO document
+    fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
+        visit_schema_object(self, schema)
+    }
+}
+
+/// TODO document
+pub fn visit_root_schema<V: Visitor + ?Sized>(v: &mut V, root: &mut RootSchema) {
+    v.visit_schema_object(&mut root.schema);
+    visit_map_values(v, &mut root.definitions);
+}
+
+/// TODO document
+pub fn visit_schema<V: Visitor + ?Sized>(v: &mut V, schema: &mut Schema) {
+    if let Schema::Object(schema) = schema {
+        v.visit_schema_object(schema)
+    }
+}
+
+/// TODO document
+pub fn visit_schema_object<V: Visitor + ?Sized>(v: &mut V, schema: &mut SchemaObject) {
+    if let Some(sub) = &mut schema.subschemas {
+        visit_vec(v, &mut sub.all_of);
+        visit_vec(v, &mut sub.any_of);
+        visit_vec(v, &mut sub.one_of);
+        visit_box(v, &mut sub.not);
+        visit_box(v, &mut sub.if_schema);
+        visit_box(v, &mut sub.then_schema);
+        visit_box(v, &mut sub.else_schema);
+    }
+
+    if let Some(arr) = &mut schema.array {
+        visit_single_or_vec(v, &mut arr.items);
+        visit_box(v, &mut arr.additional_items);
+        visit_box(v, &mut arr.contains);
+    }
+
+    if let Some(obj) = &mut schema.object {
+        visit_map_values(v, &mut obj.properties);
+        visit_map_values(v, &mut obj.pattern_properties);
+        visit_box(v, &mut obj.additional_properties);
+        visit_box(v, &mut obj.property_names);
+    }
+}
+
+fn visit_box<V: Visitor + ?Sized>(v: &mut V, target: &mut Option<Box<Schema>>) {
+    if let Some(s) = target {
+        v.visit_schema(s)
+    }
+}
+
+fn visit_vec<V: Visitor + ?Sized>(v: &mut V, target: &mut Option<Vec<Schema>>) {
+    if let Some(vec) = target {
+        for s in vec {
+            v.visit_schema(s)
+        }
+    }
+}
+
+fn visit_map_values<V: Visitor + ?Sized>(v: &mut V, target: &mut crate::Map<String, Schema>) {
+    for s in target.values_mut() {
+        v.visit_schema(s)
+    }
+}
+
+fn visit_single_or_vec<V: Visitor + ?Sized>(v: &mut V, target: &mut Option<SingleOrVec<Schema>>) {
+    match target {
+        None => {}
+        Some(SingleOrVec::Single(s)) => v.visit_schema(s),
+        Some(SingleOrVec::Vec(vec)) => {
+            for s in vec {
+                v.visit_schema(s)
+            }
+        }
+    }
+}
+
+/// TODO document
+#[derive(Debug, Clone)]
+pub struct ReplaceBoolSchemas {
+    pub skip_additional_properties: bool,
+}
+
+impl Visitor for ReplaceBoolSchemas {
+    fn visit_schema(&mut self, schema: &mut Schema) {
+        visit_schema(self, schema);
+
+        if let Schema::Bool(b) = *schema {
+            *schema = Schema::Bool(b).into_object().into()
+        }
+    }
+
+    fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
+        if self.skip_additional_properties {
+            if let Some(obj) = &mut schema.object {
+                if let Some(ap) = &obj.additional_properties {
+                    if let Schema::Bool(_) = ap.as_ref() {
+                        let additional_properties = obj.additional_properties.take();
+                        visit_schema_object(self, schema);
+                        schema.object().additional_properties = additional_properties;
+
+                        return;
+                    }
+                }
+            }
+        }
+
+        visit_schema_object(self, schema);
+    }
+}
+
+/// TODO document
+#[derive(Debug, Clone)]
+pub struct RemoveRefSiblings;
+
+impl Visitor for RemoveRefSiblings {
+    fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
+        visit_schema_object(self, schema);
+
+        if let Some(reference) = schema.reference.take() {
+            if schema == &SchemaObject::default() {
+                schema.reference = Some(reference);
+            } else {
+                let ref_schema = Schema::new_ref(reference);
+                let all_of = &mut schema.subschemas().all_of;
+                match all_of {
+                    Some(vec) => vec.push(ref_schema),
+                    None => *all_of = Some(vec![ref_schema]),
+                }
+            }
+        }
+    }
+}
diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs
index 9bf221c..37919b3 100644
--- a/schemars_derive/src/attr/mod.rs
+++ b/schemars_derive/src/attr/mod.rs
@@ -4,6 +4,7 @@ mod schemars_to_serde;
 pub use schemars_to_serde::process_serde_attrs;
 
 use proc_macro2::{Group, Span, TokenStream, TokenTree};
+use quote::ToTokens;
 use serde_derive_internals::Ctxt;
 use syn::parse::{self, Parse};
 use syn::Meta::{List, NameValue};
@@ -117,31 +118,30 @@ impl Attrs {
                     }
                 }
 
-                Meta(_meta_item) => {
-                    // TODO uncomment this for 0.8.0 (breaking change)
-                    //  https://github.com/GREsau/schemars/issues/18
-                    // if !ignore_errors {
-                    //     let path = meta_item
-                    //         .path()
-                    //         .into_token_stream()
-                    //         .to_string()
-                    //         .replace(' ', "");
-                    //     errors.error_spanned_by(
-                    //         meta_item.path(),
-                    //         format!("unknown schemars container attribute `{}`", path),
-                    //     );
-                    // }
+                _ if ignore_errors => {}
+
+                Meta(meta_item) => {
+                    let is_known_serde_keyword = schemars_to_serde::SERDE_KEYWORDS
+                        .iter()
+                        .any(|k| meta_item.path().is_ident(k));
+                    if !is_known_serde_keyword {
+                        let path = meta_item
+                            .path()
+                            .into_token_stream()
+                            .to_string()
+                            .replace(' ', "");
+                        errors.error_spanned_by(
+                            meta_item.path(),
+                            format!("unknown schemars container attribute `{}`", path),
+                        );
+                    }
                 }
 
-                Lit(_lit) => {
-                    // TODO uncomment this for 0.8.0 (breaking change)
-                    //  https://github.com/GREsau/schemars/issues/18
-                    // if !ignore_errors {
-                    //     errors.error_spanned_by(
-                    //         lit,
-                    //         "unexpected literal in schemars container attribute",
-                    //     );
-                    // }
+                Lit(lit) => {
+                    errors.error_spanned_by(
+                        lit,
+                        "unexpected literal in schemars container attribute",
+                    );
                 }
             }
         }
diff --git a/schemars_derive/src/attr/schemars_to_serde.rs b/schemars_derive/src/attr/schemars_to_serde.rs
index 380a276..bf8d389 100644
--- a/schemars_derive/src/attr/schemars_to_serde.rs
+++ b/schemars_derive/src/attr/schemars_to_serde.rs
@@ -5,7 +5,7 @@ use syn::parse::Parser;
 use syn::{Attribute, Data, Field, Meta, NestedMeta, Variant};
 
 // List of keywords that can appear in #[serde(...)]/#[schemars(...)] attributes which we want serde_derive_internals to parse for us.
-static SERDE_KEYWORDS: &[&str] = &[
+pub(crate) static SERDE_KEYWORDS: &[&str] = &[
     "rename",
     "rename_all",
     "deny_unknown_fields",


Test patch:
diff --git a/schemars/tests/docs.rs b/schemars/tests/docs.rs
index df07f4e..be97e34 100644
--- a/schemars/tests/docs.rs
+++ b/schemars/tests/docs.rs
@@ -58,7 +58,7 @@ fn doc_comments_struct() -> TestResult {
 
 #[test]
 fn doc_comments_struct_ref_siblings() -> TestResult {
-    let settings = SchemaSettings::draft07().with(|s| s.allow_ref_siblings = true);
+    let settings = SchemaSettings::draft2019_09();
     test_generated_schema::<MyStruct>("doc_comments_struct_ref_siblings", settings)
 }
 
diff --git a/schemars/tests/expected/doc_comments_struct_ref_siblings.json b/schemars/tests/expected/doc_comments_struct_ref_siblings.json
index c215d43..35bb648 100644
--- a/schemars/tests/expected/doc_comments_struct_ref_siblings.json
+++ b/schemars/tests/expected/doc_comments_struct_ref_siblings.json
@@ -1,5 +1,5 @@
 {
-  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
   "title": "This is the struct's title",
   "description": "This is the struct's description.",
   "type": "object",
diff --git a/schemars/tests/ui.rs b/schemars/tests/ui.rs
new file mode 100644
index 0000000..870c2f9
--- /dev/null
+++ b/schemars/tests/ui.rs
@@ -0,0 +1,5 @@
+#[test]
+fn ui() {
+    let t = trybuild::TestCases::new();
+    t.compile_fail("tests/ui/*.rs");
+}
diff --git a/schemars/tests/ui/invalid_attrs.rs b/schemars/tests/ui/invalid_attrs.rs
new file mode 100644
index 0000000..0eea93b
--- /dev/null
+++ b/schemars/tests/ui/invalid_attrs.rs
@@ -0,0 +1,11 @@
+use schemars::JsonSchema;
+
+#[derive(JsonSchema)]
+#[serde(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
+pub struct Struct1;
+
+#[derive(JsonSchema)]
+#[schemars(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
+pub struct Struct2;
+
+fn main() {}
diff --git a/schemars/tests/ui/invalid_attrs.stderr b/schemars/tests/ui/invalid_attrs.stderr
new file mode 100644
index 0000000..fa3a4f4
--- /dev/null
+++ b/schemars/tests/ui/invalid_attrs.stderr
@@ -0,0 +1,29 @@
+error: expected serde default attribute to be a string: `default = "..."`
+ --> $DIR/invalid_attrs.rs:4:19
+  |
+4 | #[serde(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
+  |                   ^
+
+error: duplicate serde attribute `deny_unknown_fields`
+ --> $DIR/invalid_attrs.rs:4:48
+  |
+4 | #[serde(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
+  |                                                ^^^^^^^^^^^^^^^^^^^
+
+error: expected serde default attribute to be a string: `default = "..."`
+ --> $DIR/invalid_attrs.rs:8:22
+  |
+8 | #[schemars(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
+  |                      ^
+
+error: duplicate serde attribute `deny_unknown_fields`
+ --> $DIR/invalid_attrs.rs:8:51
+  |
+8 | #[schemars(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
+  |                                                   ^^^^^^^^^^^^^^^^^^^
+
+error: unknown schemars container attribute `foo`
+ --> $DIR/invalid_attrs.rs:8:25
+  |
+8 | #[schemars(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
+  |                         ^^^
diff --git a/update-tests.sh b/update-tests.sh
index 476531f..0ba525c 100644
--- a/update-tests.sh
+++ b/update-tests.sh
@@ -5,7 +5,7 @@ cd schemars
 
 rm -f tests/actual/*.json
 
-cargo test --all-features --no-fail-fast --tests || :
+TRYBUILD=overwrite cargo test --all-features --no-fail-fast --tests || :
 
 if ls tests/actual/*.json 1> /dev/null 2>&1; then
   mv -f tests/actual/*.json tests/expected/