Repository: jonasbb/serde_with
Pull request #866 (2025-07-07 11:05:10)

Allow for schema derivation for the Hex type
## Description

This PR allows for the `serde_with::hex::Hex` type to be used with `schemars`

## Tests

Updated the `schemars_basic` test cases with tests for lowercase and uppercase hex schemas


Patch:
diff --git a/serde_with/src/schemars_0_8.rs b/serde_with/src/schemars_0_8.rs
index 61d9fad..472b01e 100644
--- a/serde_with/src/schemars_0_8.rs
+++ b/serde_with/src/schemars_0_8.rs
@@ -404,6 +404,35 @@ impl<T> JsonSchemaAs<T> for DisplayFromStr {
     forward_schema!(String);
 }
 
+#[cfg(feature = "hex")]
+impl<T, F: formats::Format> JsonSchemaAs<T> for hex::Hex<F> {
+    fn schema_name() -> String {
+        "Hex<F>".into()
+    }
+
+    fn schema_id() -> Cow<'static, str> {
+        "serde_with::hex::Hex<F>".into()
+    }
+
+    fn json_schema(_: &mut SchemaGenerator) -> Schema {
+        use ::schemars_0_8::schema::StringValidation;
+
+        SchemaObject {
+            instance_type: Some(InstanceType::String.into()),
+            string: Some(Box::new(StringValidation {
+                pattern: Some(r"^(?:[0-9A-Fa-f]{2})*$".to_owned()),
+                ..Default::default()
+            })),
+            ..Default::default()
+        }
+        .into()
+    }
+
+    fn is_referenceable() -> bool {
+        false
+    }
+}
+
 impl JsonSchemaAs<bool> for BoolFromInt<Strict> {
     fn schema_name() -> String {
         "BoolFromInt<Strict>".into()
diff --git a/serde_with/src/schemars_0_9.rs b/serde_with/src/schemars_0_9.rs
index b8b35f9..0619a4b 100644
--- a/serde_with/src/schemars_0_9.rs
+++ b/serde_with/src/schemars_0_9.rs
@@ -406,6 +406,28 @@ impl<T> JsonSchemaAs<T> for DisplayFromStr {
     forward_schema!(String);
 }
 
+#[cfg(feature = "hex")]
+impl<T, F: formats::Format> JsonSchemaAs<T> for hex::Hex<F> {
+    fn schema_name() -> Cow<'static, str> {
+        "Hex<F>".into()
+    }
+
+    fn schema_id() -> Cow<'static, str> {
+        "serde_with::hex::Hex<F>".into()
+    }
+
+    fn json_schema(_: &mut SchemaGenerator) -> Schema {
+        json_schema!({
+            "type": "string",
+            "pattern": r"^(?:[0-9A-Fa-f]{2})*$",
+        })
+    }
+
+    fn inline_schema() -> bool {
+        true
+    }
+}
+
 impl JsonSchemaAs<bool> for BoolFromInt<Strict> {
     fn schema_name() -> Cow<'static, str> {
         "BoolFromInt<Strict>".into()
diff --git a/serde_with/src/schemars_1.rs b/serde_with/src/schemars_1.rs
index 33f08df..7e435ac 100644
--- a/serde_with/src/schemars_1.rs
+++ b/serde_with/src/schemars_1.rs
@@ -408,6 +408,28 @@ impl<T> JsonSchemaAs<T> for DisplayFromStr {
     forward_schema!(String);
 }
 
+#[cfg(feature = "hex")]
+impl<T, F: formats::Format> JsonSchemaAs<T> for hex::Hex<F> {
+    fn schema_name() -> Cow<'static, str> {
+        "Hex<F>".into()
+    }
+
+    fn schema_id() -> Cow<'static, str> {
+        "serde_with::hex::Hex<F>".into()
+    }
+
+    fn json_schema(_: &mut SchemaGenerator) -> Schema {
+        json_schema!({
+            "type": "string",
+            "pattern": r"^(?:[0-9A-Fa-f]{2})*$",
+        })
+    }
+
+    fn inline_schema() -> bool {
+        true
+    }
+}
+
 impl JsonSchemaAs<bool> for BoolFromInt<Strict> {
     fn schema_name() -> Cow<'static, str> {
         "BoolFromInt<Strict>".into()


Test patch:
diff --git a/serde_with/tests/schemars_0_8/main.rs b/serde_with/tests/schemars_0_8/main.rs
index 8b5dc5a..a5850c6 100644
--- a/serde_with/tests/schemars_0_8/main.rs
+++ b/serde_with/tests/schemars_0_8/main.rs
@@ -5,6 +5,7 @@ use expect_test::expect_file;
 use schemars::JsonSchema;
 use serde::Serialize;
 use serde_json::json;
+use serde_with::hex::*;
 use serde_with::*;
 use std::collections::BTreeSet;
 
@@ -90,6 +91,14 @@ fn schemars_basic() {
         /// Same thing, but with a Vec this time.
         #[serde_as(as = "Vec<_>")]
         vec_same: Vec<u32>,
+
+        /// A vector of bytes that's serialized as a lowercase hex string.
+        #[serde_as(as = "Hex")]
+        lowercase_hex: Vec<u8>,
+
+        /// A vector of bytes that's serialized as an uppercase hex string.
+        #[serde_as(as = "Hex<formats::Uppercase>")]
+        uppercase_hex: Vec<u8>,
     }
 
     let schema = schemars::schema_for!(Basic);
diff --git a/serde_with/tests/schemars_0_8/schemars_basic.json b/serde_with/tests/schemars_0_8/schemars_basic.json
index aafdaf2..d9505ce 100644
--- a/serde_with/tests/schemars_0_8/schemars_basic.json
+++ b/serde_with/tests/schemars_0_8/schemars_basic.json
@@ -6,7 +6,9 @@
     "bare_field",
     "box_same",
     "display_from_str",
+    "lowercase_hex",
     "same",
+    "uppercase_hex",
     "vec_same"
   ],
   "properties": {
@@ -26,12 +28,22 @@
       "description": "Field that directly uses `DisplayFromStr`",
       "type": "string"
     },
+    "lowercase_hex": {
+      "description": "A vector of bytes that's serialized as a lowercase hex string.",
+      "type": "string",
+      "pattern": "^(?:[0-9A-Fa-f]{2})*$"
+    },
     "same": {
       "description": "Same does not implement `JsonSchema` directly so this checks that the correct schemars attribute was injected.",
       "type": "integer",
       "format": "uint32",
       "minimum": 0.0
     },
+    "uppercase_hex": {
+      "description": "A vector of bytes that's serialized as an uppercase hex string.",
+      "type": "string",
+      "pattern": "^(?:[0-9A-Fa-f]{2})*$"
+    },
     "vec_same": {
       "description": "Same thing, but with a Vec this time.",
       "type": "array",
diff --git a/serde_with/tests/schemars_0_9/main.rs b/serde_with/tests/schemars_0_9/main.rs
index eb417cd..c28b872 100644
--- a/serde_with/tests/schemars_0_9/main.rs
+++ b/serde_with/tests/schemars_0_9/main.rs
@@ -6,6 +6,7 @@ use expect_test::expect_file;
 use schemars::JsonSchema;
 use serde::Serialize;
 use serde_json::json;
+use serde_with::hex::*;
 use serde_with::*;
 use std::collections::BTreeSet;
 
@@ -91,6 +92,14 @@ fn schemars_basic() {
         /// Same thing, but with a Vec this time.
         #[serde_as(as = "Vec<_>")]
         vec_same: Vec<u32>,
+
+        /// A vector of bytes that's serialized as a lowercase hex string.
+        #[serde_as(as = "Hex")]
+        lowercase_hex: Vec<u8>,
+
+        /// A vector of bytes that's serialized as an uppercase hex string.
+        #[serde_as(as = "Hex<formats::Uppercase>")]
+        uppercase_hex: Vec<u8>,
     }
 
     let schema = schemars::schema_for!(Basic);
diff --git a/serde_with/tests/schemars_0_9/schemars_basic.json b/serde_with/tests/schemars_0_9/schemars_basic.json
index 340ca28..49c3922 100644
--- a/serde_with/tests/schemars_0_9/schemars_basic.json
+++ b/serde_with/tests/schemars_0_9/schemars_basic.json
@@ -33,6 +33,16 @@
         "format": "uint32",
         "minimum": 0
       }
+    },
+    "lowercase_hex": {
+      "description": "A vector of bytes that's serialized as a lowercase hex string.",
+      "type": "string",
+      "pattern": "^(?:[0-9A-Fa-f]{2})*$"
+    },
+    "uppercase_hex": {
+      "description": "A vector of bytes that's serialized as an uppercase hex string.",
+      "type": "string",
+      "pattern": "^(?:[0-9A-Fa-f]{2})*$"
     }
   },
   "required": [
@@ -40,6 +50,8 @@
     "display_from_str",
     "same",
     "box_same",
-    "vec_same"
+    "vec_same",
+    "lowercase_hex",
+    "uppercase_hex"
   ]
 }
diff --git a/serde_with/tests/schemars_1/main.rs b/serde_with/tests/schemars_1/main.rs
index 8397906..25592f6 100644
--- a/serde_with/tests/schemars_1/main.rs
+++ b/serde_with/tests/schemars_1/main.rs
@@ -6,6 +6,7 @@ use expect_test::expect_file;
 use schemars::JsonSchema;
 use serde::Serialize;
 use serde_json::json;
+use serde_with::hex::*;
 use serde_with::*;
 use std::collections::BTreeSet;
 
@@ -91,6 +92,14 @@ fn schemars_basic() {
         /// Same thing, but with a Vec this time.
         #[serde_as(as = "Vec<_>")]
         vec_same: Vec<u32>,
+
+        /// A vector of bytes that's serialized as a lowercase hex string.
+        #[serde_as(as = "Hex")]
+        lowercase_hex: Vec<u8>,
+
+        /// A vector of bytes that's serialized as an uppercase hex string.
+        #[serde_as(as = "Hex<formats::Uppercase>")]
+        uppercase_hex: Vec<u8>,
     }
 
     let schema = schemars::schema_for!(Basic);
diff --git a/serde_with/tests/schemars_1/schemars_basic.json b/serde_with/tests/schemars_1/schemars_basic.json
index aaaf49f..c4338b5 100644
--- a/serde_with/tests/schemars_1/schemars_basic.json
+++ b/serde_with/tests/schemars_1/schemars_basic.json
@@ -33,6 +33,16 @@
         "format": "uint32",
         "minimum": 0
       }
+    },
+    "lowercase_hex": {
+      "description": "A vector of bytes that's serialized as a lowercase hex string.",
+      "type": "string",
+      "pattern": "^(?:[0-9A-Fa-f]{2})*$"
+    },
+    "uppercase_hex": {
+      "description": "A vector of bytes that's serialized as an uppercase hex string.",
+      "type": "string",
+      "pattern": "^(?:[0-9A-Fa-f]{2})*$"
     }
   },
   "required": [
@@ -40,6 +50,8 @@
     "display_from_str",
     "same",
     "box_same",
-    "vec_same"
+    "vec_same",
+    "lowercase_hex",
+    "uppercase_hex"
   ]
 }