Repository: GREsau/schemars
Pull request #406 (2025-05-25 21:31:26)

Add `include_type_name` setting for setting `x-rust-type` schema property


Patch:
diff --git a/schemars/src/generate.rs b/schemars/src/generate.rs
index b1bde99..cb52ee4 100644
--- a/schemars/src/generate.rs
+++ b/schemars/src/generate.rs
@@ -26,6 +26,7 @@ type CowStr = alloc::borrow::Cow<'static, str>;
 /// [`SchemaSettings::draft2020_12()`] method.
 #[derive(Debug, Clone)]
 #[non_exhaustive]
+#[allow(clippy::struct_excessive_bools)]
 pub struct SchemaSettings {
     /// This option is now ignored and will be removed before schemars 1.0 becomes stable.
     ///
@@ -60,6 +61,21 @@ pub struct SchemaSettings {
     ///
     /// Defaults to `Contract::Deserialize`.
     pub contract: Contract,
+    /// Whether to include an `"x-rust-type"` property in schemas, set to the name of the
+    /// schema's associated rust type.
+    ///
+    /// This uses [`std::any::type_name()`], so shares the caveats of that function. In particular,
+    /// the exact output is not guaranteed, and may change between versions of the rust compiler.
+    ///
+    /// When this is used with `#[derive(JsonSchema)]`, any fields with the
+    /// `#[serde(with = "Type")]`/`#[schemars(with = "Type")]` attribute will use name of the type
+    /// from the attribute, **not** the type of the actual field. Similarly, any fields with the
+    /// `#[schemars(schema_with = "function")]` attribute will not have the `"x-rust-type"`
+    /// property added (you can, of course, set the property to whatever you like in the function
+    /// implementation).
+    ///
+    /// Defaults to `false`.
+    pub include_type_name: bool,
 }
 
 impl Default for SchemaSettings {
@@ -87,6 +103,7 @@ impl SchemaSettings {
             ],
             inline_subschemas: false,
             contract: Contract::Deserialize,
+            include_type_name: false,
         }
     }
 
@@ -101,6 +118,7 @@ impl SchemaSettings {
             transforms: vec![Box::new(ReplacePrefixItems)],
             inline_subschemas: false,
             contract: Contract::Deserialize,
+            include_type_name: false,
         }
     }
 
@@ -115,6 +133,7 @@ impl SchemaSettings {
             transforms: Vec::new(),
             inline_subschemas: false,
             contract: Contract::Deserialize,
+            include_type_name: false,
         }
     }
 
@@ -139,6 +158,7 @@ impl SchemaSettings {
             ],
             inline_subschemas: false,
             contract: Contract::Deserialize,
+            include_type_name: false,
         }
     }
 
@@ -541,7 +561,15 @@ impl SchemaGenerator {
         }
 
         let pss = PendingSchemaState::new(self, uid);
-        T::json_schema(pss.generator)
+        let mut schema = T::json_schema(pss.generator);
+
+        if pss.generator.settings().include_type_name
+            && !pss.uid.0.starts_with("_SchemarsSchemaWithFunction/")
+        {
+            schema.insert("x-rust-type".to_owned(), core::any::type_name::<T>().into());
+        }
+
+        schema
     }
 
     fn add_definitions(


Test patch:
diff --git a/schemars/tests/integration/settings.rs b/schemars/tests/integration/settings.rs
index 3434d4f..4b2cb02 100644
--- a/schemars/tests/integration/settings.rs
+++ b/schemars/tests/integration/settings.rs
@@ -1,6 +1,7 @@
 use crate::prelude::*;
 use pretty_assertions::assert_eq;
-use schemars::{generate::SchemaSettings, Schema};
+use schemars::{generate::SchemaSettings, Schema, SchemaGenerator};
+use std::any::type_name;
 
 #[derive(JsonSchema, Deserialize, Serialize, Default)]
 #[schemars(inline)]
@@ -80,3 +81,63 @@ fn openapi3() {
 
     assert_eq!(definitions1, &definitions2);
 }
+
+#[test]
+fn include_type_name() {
+    test!(
+        OuterStruct,
+        SchemaSettings::default().with(|s| s.include_type_name = true)
+    )
+    .assert_snapshot();
+}
+
+#[test]
+fn include_type_name_handles_with_attributes() {
+    fn dummy(_: &mut SchemaGenerator) -> Schema {
+        Schema::default()
+    }
+
+    #[derive(JsonSchema)]
+    struct Foo {
+        _int: i32,
+        _value: Value,
+        #[schemars(with = "f64")]
+        _with_type: i32,
+        #[schemars(schema_with = "dummy")]
+        _with_fn: i32,
+    }
+
+    let schema = SchemaSettings::default()
+        .with(|s| s.include_type_name = true)
+        .into_generator()
+        .into_root_schema_for::<Foo>();
+
+    assert_eq!(schema.as_value()["x-rust-type"], type_name::<Foo>());
+    assert_eq!(
+        schema
+            .as_value()
+            .pointer("/properties/_int/x-rust-type")
+            .and_then(Value::as_str),
+        Some(type_name::<i32>())
+    );
+    assert_eq!(
+        schema
+            .as_value()
+            .pointer("/properties/_value/x-rust-type")
+            .and_then(Value::as_str),
+        Some(type_name::<Value>())
+    );
+    assert_eq!(
+        schema
+            .as_value()
+            .pointer("/properties/_with_type/x-rust-type")
+            .and_then(Value::as_str),
+        Some(type_name::<f64>())
+    );
+    assert_eq!(
+        schema
+            .as_value()
+            .pointer("/properties/_with_fn/x-rust-type"),
+        None,
+    );
+}
diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~include_type_name.de.json b/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~include_type_name.de.json
new file mode 100644
index 0000000..10dcde9
--- /dev/null
+++ b/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~include_type_name.de.json
@@ -0,0 +1,105 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "title": "OuterStruct",
+  "type": "object",
+  "properties": {
+    "maybe_int": {
+      "type": [
+        "integer",
+        "null"
+      ],
+      "format": "int32",
+      "x-rust-type": "core::option::Option<i32>",
+      "examples": [
+        8,
+        null
+      ]
+    },
+    "values": {
+      "type": "object",
+      "additionalProperties": {
+        "x-rust-type": "serde_json::value::Value"
+      },
+      "x-rust-type": "serde_json::map::Map<alloc::string::String, serde_json::value::Value>"
+    },
+    "value": {
+      "x-rust-type": "serde_json::value::Value"
+    },
+    "inner": {
+      "$ref": "#/$defs/InnerEnum"
+    },
+    "maybe_inner": {
+      "anyOf": [
+        {
+          "$ref": "#/$defs/InnerEnum"
+        },
+        {
+          "type": "null"
+        }
+      ],
+      "x-rust-type": "core::option::Option<integration::settings::InnerEnum>"
+    },
+    "tuples": {
+      "type": "array",
+      "items": {
+        "type": "array",
+        "prefixItems": [
+          {
+            "type": "integer",
+            "format": "uint8",
+            "minimum": 0,
+            "maximum": 255,
+            "x-rust-type": "u8"
+          },
+          {
+            "type": "integer",
+            "format": "int64",
+            "x-rust-type": "i64"
+          }
+        ],
+        "minItems": 2,
+        "maxItems": 2,
+        "x-rust-type": "(u8, i64)"
+      },
+      "x-rust-type": "alloc::vec::Vec<(u8, i64)>"
+    }
+  },
+  "required": [
+    "values",
+    "value",
+    "inner",
+    "tuples"
+  ],
+  "x-rust-type": "integration::settings::OuterStruct",
+  "$defs": {
+    "InnerEnum": {
+      "oneOf": [
+        {
+          "type": "string",
+          "enum": [
+            "UndocumentedUnit1",
+            "UndocumentedUnit2"
+          ]
+        },
+        {
+          "description": "This is a documented unit variant",
+          "type": "string",
+          "const": "DocumentedUnit"
+        },
+        {
+          "type": "object",
+          "properties": {
+            "ValueNewType": {
+              "x-rust-type": "serde_json::value::Value"
+            }
+          },
+          "required": [
+            "ValueNewType"
+          ],
+          "additionalProperties": false
+        }
+      ],
+      "x-rust-type": "integration::settings::InnerEnum"
+    }
+  }
+}
\ No newline at end of file
diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~include_type_name.ser.json b/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~include_type_name.ser.json
new file mode 100644
index 0000000..9066201
--- /dev/null
+++ b/schemars/tests/integration/snapshots/schemars/tests/integration/settings.rs~include_type_name.ser.json
@@ -0,0 +1,107 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "title": "OuterStruct",
+  "type": "object",
+  "properties": {
+    "maybe_int": {
+      "type": [
+        "integer",
+        "null"
+      ],
+      "format": "int32",
+      "x-rust-type": "core::option::Option<i32>",
+      "examples": [
+        8,
+        null
+      ]
+    },
+    "values": {
+      "type": "object",
+      "additionalProperties": {
+        "x-rust-type": "serde_json::value::Value"
+      },
+      "x-rust-type": "serde_json::map::Map<alloc::string::String, serde_json::value::Value>"
+    },
+    "value": {
+      "x-rust-type": "serde_json::value::Value"
+    },
+    "inner": {
+      "$ref": "#/$defs/InnerEnum"
+    },
+    "maybe_inner": {
+      "anyOf": [
+        {
+          "$ref": "#/$defs/InnerEnum"
+        },
+        {
+          "type": "null"
+        }
+      ],
+      "x-rust-type": "core::option::Option<integration::settings::InnerEnum>"
+    },
+    "tuples": {
+      "type": "array",
+      "items": {
+        "type": "array",
+        "prefixItems": [
+          {
+            "type": "integer",
+            "format": "uint8",
+            "minimum": 0,
+            "maximum": 255,
+            "x-rust-type": "u8"
+          },
+          {
+            "type": "integer",
+            "format": "int64",
+            "x-rust-type": "i64"
+          }
+        ],
+        "minItems": 2,
+        "maxItems": 2,
+        "x-rust-type": "(u8, i64)"
+      },
+      "x-rust-type": "alloc::vec::Vec<(u8, i64)>"
+    }
+  },
+  "required": [
+    "maybe_int",
+    "values",
+    "value",
+    "inner",
+    "maybe_inner",
+    "tuples"
+  ],
+  "x-rust-type": "integration::settings::OuterStruct",
+  "$defs": {
+    "InnerEnum": {
+      "oneOf": [
+        {
+          "type": "string",
+          "enum": [
+            "UndocumentedUnit1",
+            "UndocumentedUnit2"
+          ]
+        },
+        {
+          "description": "This is a documented unit variant",
+          "type": "string",
+          "const": "DocumentedUnit"
+        },
+        {
+          "type": "object",
+          "properties": {
+            "ValueNewType": {
+              "x-rust-type": "serde_json::value::Value"
+            }
+          },
+          "required": [
+            "ValueNewType"
+          ],
+          "additionalProperties": false
+        }
+      ],
+      "x-rust-type": "integration::settings::InnerEnum"
+    }
+  }
+}
\ No newline at end of file