Repository: GREsau/schemars (rust)

Issue:
Inline Option enum requires null added to the list of variant
According to the discussion here, when we have an enum which is nullable we should add null as one of the enum values.
https://github.com/json-schema-org/json-schema-spec/issues/258

Here is an example of this change being implemented in another library: https://github.com/vega/ts-json-schema-generator/issues/22

## Details

Given the following types:

```rust
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Copy, JsonSchema)]
enum FunctionalStatus {
    InService,
    OutOfService,
}

#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Clone, JsonSchema)]
struct Example {
    functional_status: Option<FunctionalStatus>
} 
```

When we generate the schema without inlining the definitions, we get 

```json
{
  "functional_status": {
    "description": "Functional status",
    "anyOf": [
      {
        "$ref": "#/definitions/FunctionalStatus"
      },
      {
        "type": "null"
      }
    ]
  },
}
```

This is correct and successfully validates request when null is given.

However, if we inline definitions we are given this schema, 

```json
{ 
  "functional_status": {
    "description": "Functional status",
    "type": [
      "string",
      "null"
    ],
    "enum": [
      "InService",
      "OutOfService"
    ]
  }
}
```

which fails to validate when you pass a `null` value with the following error `null is not one of ["InService","OutOfService"]`.

Instead, the schema that is generated should  have the following change

```patch
 { 
   "functional_status": {
     "description": "Functional status",
     "type": [
       "string",
       "null"
     ],
     "enum": [
       "InService",
-      "OutOfService"
+      "OutOfService",
+      null
     ]
   }
 }
```

Pull request:
Drive "nullable" behaviour from a new transform
The `option_nullable` and `option_add_null_type` settings were always a bit weird because they didn't change the behaviour of `SchemaGenerator`, they were only checked by the `JsonSchema` impl for `Option<T>`. This meant that if someone wanted to implement `JsonSchema` on a custom type that may map to a `null` JSON value, they would need to know to check those fields for the implementation to be correct when used for openapi 3.0 schemas.

This PR removes (or for now, deprecates and stops using) `option_nullable` and `option_add_null_type`. Now, `Option<T>` always adds a null type, since that's the standard for JSON Schema. To continue supporting openapi 3.0, this PR also adds a new transform `AddNullable`. This, as the name suggests, adds the `nullable` property to schemas that allow the null type, and (optionally) remove the null type.

The new helper for generating nullable schemas will now also correctly update schemas with `const` or `enum` properties, which fixes #382.

Misc changes in this PR not directly related to the main change:
- Make all transforms `#[non_exhaustive]` to allow more settings to be added to them later if necessary
- Implement `PartialEq` between `Schema` and `bool`/`Value`/`Map<String, Value>` and vice-versa

Patch:
diff --git a/docs/_includes/examples/custom_settings.rs b/docs/_includes/examples/custom_settings.rs
index 7858720..e6eff2e 100644
--- a/docs/_includes/examples/custom_settings.rs
+++ b/docs/_includes/examples/custom_settings.rs
@@ -15,8 +15,8 @@ pub enum MyEnum {
 
 fn main() {
     let settings = SchemaSettings::draft07().with(|s| {
-        s.option_nullable = true;
-        s.option_add_null_type = false;
+        s.meta_schema = None;
+        s.inline_subschemas = true;
     });
     let generator = settings.into_generator();
     let schema = generator.into_root_schema_for::<MyStruct>();
diff --git a/docs/_includes/examples/custom_settings.schema.json b/docs/_includes/examples/custom_settings.schema.json
index 8da4348..727f798 100644
--- a/docs/_includes/examples/custom_settings.schema.json
+++ b/docs/_includes/examples/custom_settings.schema.json
@@ -1,5 +1,4 @@
 {
-  "$schema": "http://json-schema.org/draft-07/schema#",
   "title": "MyStruct",
   "type": "object",
   "properties": {
@@ -11,58 +10,55 @@
       "format": "int32"
     },
     "my_nullable_enum": {
-      "allOf": [
+      "anyOf": [
         {
-          "$ref": "#/definitions/MyEnum"
-        }
-      ],
-      "nullable": true
-    }
-  },
-  "required": [
-    "my_int",
-    "my_bool"
-  ],
-  "definitions": {
-    "MyEnum": {
-      "oneOf": [
-        {
-          "type": "object",
-          "properties": {
-            "StringNewType": {
-              "type": "string"
-            }
-          },
-          "additionalProperties": false,
-          "required": [
-            "StringNewType"
-          ]
-        },
-        {
-          "type": "object",
-          "properties": {
-            "StructVariant": {
+          "oneOf": [
+            {
               "type": "object",
               "properties": {
-                "floats": {
-                  "type": "array",
-                  "items": {
-                    "type": "number",
-                    "format": "float"
-                  }
+                "StringNewType": {
+                  "type": "string"
                 }
               },
+              "additionalProperties": false,
               "required": [
-                "floats"
+                "StringNewType"
+              ]
+            },
+            {
+              "type": "object",
+              "properties": {
+                "StructVariant": {
+                  "type": "object",
+                  "properties": {
+                    "floats": {
+                      "type": "array",
+                      "items": {
+                        "type": "number",
+                        "format": "float"
+                      }
+                    }
+                  },
+                  "required": [
+                    "floats"
+                  ]
+                }
+              },
+              "additionalProperties": false,
+              "required": [
+                "StructVariant"
               ]
             }
-          },
-          "additionalProperties": false,
-          "required": [
-            "StructVariant"
           ]
+        },
+        {
+          "type": "null"
         }
       ]
     }
-  }
+  },
+  "required": [
+    "my_int",
+    "my_bool"
+  ]
 }
diff --git a/schemars/examples/custom_settings.rs b/schemars/examples/custom_settings.rs
index 7858720..e6eff2e 100644
--- a/schemars/examples/custom_settings.rs
+++ b/schemars/examples/custom_settings.rs
@@ -15,8 +15,8 @@ pub enum MyEnum {
 
 fn main() {
     let settings = SchemaSettings::draft07().with(|s| {
-        s.option_nullable = true;
-        s.option_add_null_type = false;
+        s.meta_schema = None;
+        s.inline_subschemas = true;
     });
     let generator = settings.into_generator();
     let schema = generator.into_root_schema_for::<MyStruct>();
diff --git a/schemars/examples/custom_settings.schema.json b/schemars/examples/custom_settings.schema.json
index 8da4348..727f798 100644
--- a/schemars/examples/custom_settings.schema.json
+++ b/schemars/examples/custom_settings.schema.json
@@ -1,5 +1,4 @@
 {
-  "$schema": "http://json-schema.org/draft-07/schema#",
   "title": "MyStruct",
   "type": "object",
   "properties": {
@@ -11,58 +10,55 @@
       "format": "int32"
     },
     "my_nullable_enum": {
-      "allOf": [
+      "anyOf": [
         {
-          "$ref": "#/definitions/MyEnum"
-        }
-      ],
-      "nullable": true
-    }
-  },
-  "required": [
-    "my_int",
-    "my_bool"
-  ],
-  "definitions": {
-    "MyEnum": {
-      "oneOf": [
-        {
-          "type": "object",
-          "properties": {
-            "StringNewType": {
-              "type": "string"
-            }
-          },
-          "additionalProperties": false,
-          "required": [
-            "StringNewType"
-          ]
-        },
-        {
-          "type": "object",
-          "properties": {
-            "StructVariant": {
+          "oneOf": [
+            {
               "type": "object",
               "properties": {
-                "floats": {
-                  "type": "array",
-                  "items": {
-                    "type": "number",
-                    "format": "float"
-                  }
+                "StringNewType": {
+                  "type": "string"
                 }
               },
+              "additionalProperties": false,
               "required": [
-                "floats"
+                "StringNewType"
+              ]
+            },
+            {
+              "type": "object",
+              "properties": {
+                "StructVariant": {
+                  "type": "object",
+                  "properties": {
+                    "floats": {
+                      "type": "array",
+                      "items": {
+                        "type": "number",
+                        "format": "float"
+                      }
+                    }
+                  },
+                  "required": [
+                    "floats"
+                  ]
+                }
+              },
+              "additionalProperties": false,
+              "required": [
+                "StructVariant"
               ]
             }
-          },
-          "additionalProperties": false,
-          "required": [
-            "StructVariant"
           ]
+        },
+        {
+          "type": "null"
         }
       ]
     }
-  }
+  },
+  "required": [
+    "my_int",
+    "my_bool"
+  ]
 }
diff --git a/schemars/src/_private/mod.rs b/schemars/src/_private/mod.rs
index b2262ca..439f34c 100644
--- a/schemars/src/_private/mod.rs
+++ b/schemars/src/_private/mod.rs
@@ -329,7 +329,84 @@ fn normalise_additional_unevaluated_properties(
 }
 
 fn contains_immediate_subschema(schema_obj: &Map<String, Value>) -> bool {
-    ["if", "then", "else", "allOf", "anyOf", "oneOf", "$ref"]
+    ["if", "allOf", "anyOf", "oneOf", "$ref"]
         .into_iter()
         .any(|k| schema_obj.contains_key(k))
 }
+
+pub(crate) fn allow_null(generator: &mut SchemaGenerator, schema: &mut Schema) {
+    fn is_null_schema(value: &Value) -> bool {
+        <&Schema>::try_from(value).is_ok_and(|s| s.has_type("null"))
+    }
+
+    match schema.try_as_object_mut() {
+        Ok(obj) => {
+            if obj.len() == 1
+                && obj
+                    .get("anyOf")
+                    .and_then(Value::as_array)
+                    .is_some_and(|a| a.iter().any(is_null_schema))
+            {
+                return;
+            }
+
+            if contains_immediate_subschema(obj) {
+                *schema = json_schema!({
+                    "anyOf": [
+                        obj,
+                        <()>::json_schema(generator)
+                    ]
+                });
+                // No need to check `type`/`const`/`enum` because they're trivially not present
+                return;
+            }
+
+            if let Some(instance_type) = obj.get_mut("type") {
+                match instance_type {
+                    Value::Array(array) => {
+                        let null = Value::from("null");
+                        if !array.contains(&null) {
+                            array.push(null);
+                        }
+                    }
+                    Value::String(string) => {
+                        if string != "null" {
+                            let current_type = core::mem::take(string).into();
+                            *instance_type = Value::Array(vec![current_type, "null".into()]);
+                        }
+                    }
+                    _ => {}
+                }
+            }
+
+            if let Some(c) = obj.remove("const") {
+                if !c.is_null() {
+                    obj.insert("enum".to_string(), Value::Array(vec![c, Value::Null]));
+                }
+            } else if let Some(Value::Array(e)) = obj.get_mut("enum") {
+                if !e.contains(&Value::Null) {
+                    e.push(Value::Null);
+                }
+            }
+        }
+        Err(true) => {}
+        Err(false) => {
+            *schema = <()>::json_schema(generator);
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn nested_option_schemas() {
+        let mut option_schema = schema_for!(Option<Result<i8, u8>>);
+        option_schema.remove("title");
+        let mut nested_option_schema = schema_for!(Option<Option<Option<Result<i8, u8>>>>);
+        nested_option_schema.remove("title");
+
+        assert_eq!(option_schema, nested_option_schema);
+    }
+}
diff --git a/schemars/src/generate.rs b/schemars/src/generate.rs
index 9803703..8b6846a 100644
--- a/schemars/src/generate.rs
+++ b/schemars/src/generate.rs
@@ -26,15 +26,15 @@ type CowStr = alloc::borrow::Cow<'static, str>;
 #[derive(Debug, Clone)]
 #[non_exhaustive]
 pub struct SchemaSettings {
-    /// If `true`, schemas for [`Option<T>`] will include a `nullable` property.
+    /// This option is now ignored and will be removed before schemars 1.0 becomes stable.
     ///
-    /// This is not part of the JSON Schema spec, but is used in Swagger/OpenAPI schemas.
-    ///
-    /// Defaults to `false`.
+    /// If you need the `nullable` keyword to be added to schemas for [`Option<T>`], you should use the [`AddNullable`] transform.
+    #[deprecated = "This option is now ignored. If you need the `nullable` keyword to be added to schemas for `Option<T>`, you should use the `AddNullable` transform."]
     pub option_nullable: bool,
-    /// If `true`, schemas for [`Option<T>`] will have `null` added to their `type` property.
+    /// This option is now ignored and will be removed before schemars 1.0 becomes stable.
     ///
-    /// Defaults to `true`.
+    /// The null type is now always added to schema for [`Option<T>`], but may be removed if using the [`AddNullable`] transform.
+    #[deprecated = "This option is now ignored."]
     pub option_add_null_type: bool,
     /// A JSON pointer to the expected location of referenceable subschemas within the resulting
     /// root schema.
@@ -62,9 +62,9 @@ pub struct SchemaSettings {
 }
 
 impl Default for SchemaSettings {
-    /// The default settings currently conform to [JSON Schema 2020-12](https://json-schema.org/specification-links#2020-12), but this is liable to change in a future version of Schemars if support for other JSON Schema versions is added.
-    /// If you rely on generated schemas conforming to draft 2020-12, consider using the
-    /// [`SchemaSettings::draft2020_12()`] method.
+    /// The default settings currently conform to [JSON Schema 2020-12](https://json-schema.org/specification-links#2020-12),
+    /// but this is liable to change in a future version of Schemars if support for other JSON Schema versions is added.
+    /// If you rely on generated schemas conforming to draft 2020-12, consider using [`SchemaSettings::draft2020_12()`] instead.
     fn default() -> SchemaSettings {
         SchemaSettings::draft2020_12()
     }
@@ -73,6 +73,7 @@ impl Default for SchemaSettings {
 impl SchemaSettings {
     /// Creates `SchemaSettings` that conform to [JSON Schema Draft 7](https://json-schema.org/specification-links#draft-7).
     pub fn draft07() -> SchemaSettings {
+        #[allow(deprecated)]
         SchemaSettings {
             option_nullable: false,
             option_add_null_type: true,
@@ -90,6 +91,7 @@ impl SchemaSettings {
 
     /// Creates `SchemaSettings` that conform to [JSON Schema 2019-09](https://json-schema.org/specification-links#draft-2019-09-(formerly-known-as-draft-8)).
     pub fn draft2019_09() -> SchemaSettings {
+        #[allow(deprecated)]
         SchemaSettings {
             option_nullable: false,
             option_add_null_type: true,
@@ -103,6 +105,7 @@ impl SchemaSettings {
 
     /// Creates `SchemaSettings` that conform to [JSON Schema 2020-12](https://json-schema.org/specification-links#2020-12).
     pub fn draft2020_12() -> SchemaSettings {
+        #[allow(deprecated)]
         SchemaSettings {
             option_nullable: false,
             option_add_null_type: true,
@@ -116,6 +119,7 @@ impl SchemaSettings {
 
     /// Creates `SchemaSettings` that conform to [OpenAPI 3.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schema).
     pub fn openapi3() -> SchemaSettings {
+        #[allow(deprecated)]
         SchemaSettings {
             option_nullable: true,
             option_add_null_type: false,
@@ -126,10 +130,11 @@ impl SchemaSettings {
             ),
             transforms: vec![
                 Box::new(ReplaceUnevaluatedProperties),
-                Box::new(RemoveRefSiblings),
                 Box::new(ReplaceBoolSchemas {
                     skip_additional_properties: true,
                 }),
+                Box::new(AddNullable::default()),
+                Box::new(RemoveRefSiblings),
                 Box::new(SetSingleExample),
                 Box::new(ReplaceConstValue),
                 Box::new(ReplacePrefixItems),
@@ -146,8 +151,8 @@ impl SchemaSettings {
     /// use schemars::generate::{SchemaGenerator, SchemaSettings};
     ///
     /// let settings = SchemaSettings::default().with(|s| {
-    ///     s.option_nullable = true;
-    ///     s.option_add_null_type = false;
+    ///     s.meta_schema = None;
+    ///     s.inline_subschemas = true;
     /// });
     /// let generator = settings.into_generator();
     /// ```
@@ -223,7 +228,7 @@ struct SchemaUid(CowStr, Contract);
 /// let generator = SchemaGenerator::default();
 /// let schema = generator.into_root_schema_for::<MyStruct>();
 /// ```
-#[derive(Debug, Default)]
+#[derive(Debug)]
 pub struct SchemaGenerator {
     settings: SchemaSettings,
     definitions: JsonMap<String, Value>,
@@ -232,6 +237,12 @@ pub struct SchemaGenerator {
     used_schema_names: BTreeSet<CowStr>,
 }
 
+impl Default for SchemaGenerator {
+    fn default() -> Self {
+        SchemaSettings::default().into_generator()
+    }
+}
+
 impl Clone for SchemaGenerator {
     fn clone(&self) -> Self {
         Self {
@@ -255,7 +266,10 @@ impl SchemaGenerator {
     pub fn new(settings: SchemaSettings) -> SchemaGenerator {
         SchemaGenerator {
             settings,
-            ..Default::default()
+            definitions: JsonMap::new(),
+            pending_schema_ids: BTreeSet::new(),
+            schema_id_to_name: BTreeMap::new(),
+            used_schema_names: BTreeSet::new(),
         }
     }
 
diff --git a/schemars/src/json_schema_impls/core.rs b/schemars/src/json_schema_impls/core.rs
index 68bed2e..936e740 100644
--- a/schemars/src/json_schema_impls/core.rs
+++ b/schemars/src/json_schema_impls/core.rs
@@ -1,9 +1,9 @@
 use crate::SchemaGenerator;
 use crate::_alloc_prelude::*;
+use crate::_private::allow_null;
 use crate::{json_schema, JsonSchema, Schema};
 use alloc::borrow::Cow;
 use core::ops::{Bound, Range, RangeInclusive};
-use serde_json::Value;
 
 impl<T: JsonSchema> JsonSchema for Option<T> {
     inline_schema!();
@@ -19,45 +19,7 @@ impl<T: JsonSchema> JsonSchema for Option<T> {
     fn json_schema(generator: &mut SchemaGenerator) -> Schema {
         let mut schema = generator.subschema_for::<T>();
 
-        if generator.settings().option_add_null_type {
-            schema = match schema.try_to_object() {
-                Ok(mut obj) => {
-                    let instance_type = obj.get_mut("type");
-                    match instance_type {
-                        Some(Value::Array(array)) => {
-                            let null = Value::from("null");
-                            if !array.contains(&null) {
-                                array.push(null);
-                            }
-                            obj.into()
-                        }
-                        Some(Value::String(string)) => {
-                            if string != "null" {
-                                *instance_type.unwrap() = Value::Array(vec![
-                                    core::mem::take(string).into(),
-                                    "null".into(),
-                                ]);
-                            }
-                            obj.into()
-                        }
-                        _ => json_schema!({
-                            "anyOf": [
-                                obj,
-                                <()>::json_schema(generator)
-                            ]
-                        }),
-                    }
-                }
-                Err(true) => true.into(),
-                Err(false) => <()>::json_schema(generator),
-            }
-        }
-
-        if generator.settings().option_nullable {
-            schema
-                .ensure_object()
-                .insert("nullable".into(), true.into());
-        }
+        allow_null(generator, &mut schema);
 
         schema
     }
diff --git a/schemars/src/lib.rs b/schemars/src/lib.rs
index 170b1da..adc098f 100644
--- a/schemars/src/lib.rs
+++ b/schemars/src/lib.rs
@@ -3,7 +3,9 @@
     missing_docs,
     unused_imports,
     clippy::cargo,
-    clippy::pedantic
+    clippy::pedantic,
+    clippy::exhaustive_structs,
+    clippy::exhaustive_enums
 )]
 #![allow(
     clippy::must_use_candidate,
@@ -29,6 +31,7 @@ mod macros;
 /// This module is only public for use by `schemars_derive`. It should not need to be used by code
 /// outside of `schemars`, and should not be considered part of the public API.
 #[doc(hidden)]
+#[allow(clippy::exhaustive_structs)]
 pub mod _private;
 pub mod generate;
 pub mod transform;
diff --git a/schemars/src/schema.rs b/schemars/src/schema.rs
index a0028d2..ca39366 100644
--- a/schemars/src/schema.rs
+++ b/schemars/src/schema.rs
@@ -85,6 +85,42 @@ impl Serialize for Schema {
     }
 }
 
+impl PartialEq<bool> for Schema {
+    fn eq(&self, other: &bool) -> bool {
+        self.as_bool() == Some(*other)
+    }
+}
+
+impl PartialEq<Map<String, Value>> for Schema {
+    fn eq(&self, other: &Map<String, Value>) -> bool {
+        self.as_object() == Some(other)
+    }
+}
+
+impl PartialEq<Value> for Schema {
+    fn eq(&self, other: &Value) -> bool {
+        self.as_value() == other
+    }
+}
+
+impl PartialEq<Schema> for bool {
+    fn eq(&self, other: &Schema) -> bool {
+        other == self
+    }
+}
+
+impl PartialEq<Schema> for Map<String, Value> {
+    fn eq(&self, other: &Schema) -> bool {
+        other == self
+    }
+}
+
+impl PartialEq<Schema> for Value {
+    fn eq(&self, other: &Schema) -> bool {
+        other == self
+    }
+}
+
 impl Schema {
     /// Creates a new schema object with a single string property `"$ref"`.
     ///
@@ -126,6 +162,14 @@ impl Schema {
         }
     }
 
+    pub(crate) fn try_as_object_mut(&mut self) -> Result<&mut Map<String, Value>, bool> {
+        match &mut self.0 {
+            Value::Object(m) => Ok(m),
+            Value::Bool(b) => Err(*b),
+            _ => unreachable!(),
+        }
+    }
+
     /// Returns the `Schema`'s underlying JSON value.
     pub fn to_value(self) -> Value {
         self.0
@@ -178,7 +222,7 @@ impl Schema {
     }
 
     /// If the `Schema`'s underlying JSON value is an object, gets a reference to that object's
-    /// value for the given key.
+    /// value for the given key if it exists.
     ///
     /// This always returns `None` for bool schemas.
     ///
diff --git a/schemars/src/ser.rs b/schemars/src/ser.rs
index 1227af0..81b2a55 100644
--- a/schemars/src/ser.rs
+++ b/schemars/src/ser.rs
@@ -1,5 +1,6 @@
 use crate::_alloc_prelude::*;
-use crate::{json_schema, JsonSchema, Schema, SchemaGenerator};
+use crate::_private::allow_null;
+use crate::{json_schema, Schema, SchemaGenerator};
 use core::fmt::Display;
 use serde_json::{Error, Map, Value};
 
@@ -113,7 +114,7 @@ impl<'a> serde::Serializer for Serializer<'a> {
     }
 
     fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
-        Ok(self.generator.subschema_for::<Option<Value>>())
+        Ok(self.generator.subschema_for::<Value>())
     }
 
     fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
@@ -129,45 +130,7 @@ impl<'a> serde::Serializer for Serializer<'a> {
             include_title: false,
         })?;
 
-        if self.generator.settings().option_add_null_type {
-            schema = match schema.try_to_object() {
-                Ok(mut obj) => {
-                    let value = obj.get_mut("type");
-                    match value {
-                        Some(Value::Array(array)) => {
-                            let null = Value::from("null");
-                            if !array.contains(&null) {
-                                array.push(null);
-                            }
-                            obj.into()
-                        }
-                        Some(Value::String(string)) => {
-                            if string != "null" {
-                                *value.unwrap() = Value::Array(vec![
-                                    core::mem::take(string).into(),
-                                    "null".into(),
-                                ]);
-                            }
-                            obj.into()
-                        }
-                        _ => json_schema!({
-                            "anyOf": [
-                                obj,
-                                <()>::json_schema(self.generator)
-                            ]
-                        }),
-                    }
-                }
-                Err(true) => true.into(),
-                Err(false) => <()>::json_schema(self.generator),
-            }
-        }
-
-        if self.generator.settings().option_nullable {
-            schema
-                .ensure_object()
-                .insert("nullable".into(), true.into());
-        }
+        allow_null(self.generator, &mut schema);
 
         Ok(schema)
     }
diff --git a/schemars/src/transform.rs b/schemars/src/transform.rs
index 7d24b5d..b6e0463 100644
--- a/schemars/src/transform.rs
+++ b/schemars/src/transform.rs
@@ -257,6 +257,7 @@ pub(crate) fn transform_immediate_subschemas<T: Transform + ?Sized>(
 /// );
 /// ```
 #[derive(Debug, Clone)]
+#[allow(clippy::exhaustive_structs)]
 pub struct RecursiveTransform<T>(pub T);
 
 impl<T> Transform for RecursiveTransform<T>
@@ -274,10 +275,13 @@ where
 ///
 /// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support booleans as
 /// schemas.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Default)]
+#[non_exhaustive]
 pub struct ReplaceBoolSchemas {
     /// When set to `true`, a schema's `additionalProperties` property will not be changed from a
     /// boolean.
+    ///
+    /// Defaults to `false`.
     pub skip_additional_properties: bool,
 }
 
@@ -306,7 +310,8 @@ impl Transform for ReplaceBoolSchemas {
 ///
 /// This is useful for versions of JSON Schema (e.g. Draft 7) that do not support other properties
 /// alongside `$ref`.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Default)]
+#[non_exhaustive]
 pub struct RemoveRefSiblings;
 
 impl Transform for RemoveRefSiblings {
@@ -331,7 +336,8 @@ impl Transform for RemoveRefSiblings {
 ///
 /// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `examples`
 /// property.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Default)]
+#[non_exhaustive]
 pub struct SetSingleExample;
 
 impl Transform for SetSingleExample {
@@ -351,7 +357,8 @@ impl Transform for SetSingleExample {
 ///
 /// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `const`
 /// property.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Default)]
+#[non_exhaustive]
 pub struct ReplaceConstValue;
 
 impl Transform for ReplaceConstValue {
@@ -372,7 +379,8 @@ impl Transform for ReplaceConstValue {
 ///
 /// This is useful for versions of JSON Schema (e.g. Draft 7) that do not support the `prefixItems`
 /// property.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Default)]
+#[non_exhaustive]
 pub struct ReplacePrefixItems;
 
 impl Transform for ReplacePrefixItems {
@@ -389,13 +397,76 @@ impl Transform for ReplacePrefixItems {
     }
 }
 
+/// Adds a `"nullable": true` property to schemas that allow `null` types.
+/// This also applies to subschemas.
+///
+/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that use `nullable` instead of
+/// explicit null types.
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct AddNullable {
+    /// When set to `true` (the default), `"null"` will also be removed from the schemas `type`.
+    pub remove_null_type: bool,
+    /// When set to `true` (the default), a schema that has a type only allowing `null` will also
+    /// have the equivalent `"const": null` inserted.
+    pub add_const_null: bool,
+}
+
+impl Default for AddNullable {
+    fn default() -> Self {
+        Self {
+            remove_null_type: true,
+            add_const_null: true,
+        }
+    }
+}
+
+impl Transform for AddNullable {
+    fn transform(&mut self, schema: &mut Schema) {
+        if schema.has_type("null") {
+            // has_type returned true so we know schema is an object
+            let obj = schema.as_object_mut().unwrap();
+
+            obj.insert("nullable".into(), true.into());
+
+            // has_type returned true so we know "type" exists and is a string or array
+            let ty = obj.get_mut("type").unwrap();
+            let only_allows_null =
+                ty.is_string() || ty.as_array().unwrap().iter().all(|v| v == "null");
+
+            if only_allows_null {
+                if self.add_const_null {
+                    obj.insert("const".to_string(), Value::Null);
+
+                    if self.remove_null_type {
+                        obj.remove("type");
+                    }
+                } else if self.remove_null_type {
+                    *ty = Value::Array(Vec::new());
+                }
+            } else if self.remove_null_type {
+                // We know `type` is an array containing at least one non-null type
+                let array = ty.as_array_mut().unwrap();
+                array.retain(|t| t != "null");
+
+                if array.len() == 1 {
+                    *ty = array.remove(0);
+                }
+            }
+        }
+
+        transform_subschemas(self, schema);
+    }
+}
+
 /// Replaces the `unevaluatedProperties` schema property with the `additionalProperties` property,
 /// adding properties from a schema's subschemas to its `properties` where necessary.
 /// This also applies to subschemas.
 ///
 /// This is useful for versions of JSON Schema (e.g. Draft 7) that do not support the
 /// `unevaluatedProperties` property.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Default)]
+#[non_exhaustive]
 pub struct ReplaceUnevaluatedProperties;
 
 impl Transform for ReplaceUnevaluatedProperties {


Test patch:
diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/from_value.rs~custom_struct_openapi3.json b/schemars/tests/integration/snapshots/schemars/tests/integration/from_value.rs~custom_struct_openapi3.json
index 7e52acd..c7a4ded 100644
--- a/schemars/tests/integration/snapshots/schemars/tests/integration/from_value.rs~custom_struct_openapi3.json
+++ b/schemars/tests/integration/snapshots/schemars/tests/integration/from_value.rs~custom_struct_openapi3.json
@@ -9,9 +9,7 @@
     "myBool": {
       "type": "boolean"
     },
-    "myNullableEnum": {
-      "nullable": true
-    },
+    "myNullableEnum": {},
     "myInnerStruct": {
       "type": "object",
       "properties": {
diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~openapi3.de.json b/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~openapi3.de.json
index a31b385..0a93bee 100644
--- a/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~openapi3.de.json
+++ b/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~openapi3.de.json
@@ -18,10 +18,15 @@
       "$ref": "#/components/schemas/InnerEnum"
     },
     "maybe_inner": {
-      "nullable": true,
-      "allOf": [
+      "anyOf": [
         {
           "$ref": "#/components/schemas/InnerEnum"
+        },
+        {
+          "nullable": true,
+          "enum": [
+            null
+          ]
         }
       ]
     },
diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~openapi3.ser.json b/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~openapi3.ser.json
index 217541a..0401add 100644
--- a/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~openapi3.ser.json
+++ b/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~openapi3.ser.json
@@ -18,10 +18,15 @@
       "$ref": "#/components/schemas/InnerEnum"
     },
     "maybe_inner": {
-      "nullable": true,
-      "allOf": [
+      "anyOf": [
         {
           "$ref": "#/components/schemas/InnerEnum"
+        },
+        {
+          "nullable": true,
+          "enum": [
+            null
+          ]
         }
       ]
     },