Repository: GREsau/schemars
Pull request #364 (2025-01-11 04:16:25)

feat: support jiff types
This closes https://github.com/GREsau/schemars/issues/359.

cc @GREsau 

FYI @BurntSushi - I'd appreciate it if you have some time to see if this integration make correct assumption. And I noticed that jiff's weekday doesn't implement ser/de while chrono's does, perhaps it's a new feature candidate to consider.

tisonkun: @GREsau Verify this patch works well in our use case. Would you give it a review?
tisonkun: @GREsau Merge latest master. To avoid further conflict, may you take a look and see if we'd go in this direction.
tisonkun: Since Jiff releases 0.2, I've updated all the refs to jiff02.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f6ce93..5373ec9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## [0.8.22] - 2025-02-25
+
+### Fixed:
+
+- Fix compatibility with rust 2024 edition (https://github.com/GREsau/schemars/pull/378)
+
 ## [1.0.0-alpha.17] - 2024-12-02
 
 ### Changed
diff --git a/Cargo.lock b/Cargo.lock
index 972cdfe..f2d55ae 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -139,9 +139,9 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce"
 
 [[package]]
 name = "bytes"
-version = "1.9.0"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
+checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
 dependencies = [
  "serde",
 ]
@@ -245,9 +245,9 @@ dependencies = [
 
 [[package]]
 name = "dyn-clone"
-version = "1.0.17"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
+checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35"
 
 [[package]]
 name = "either"
@@ -537,6 +537,35 @@ version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
 
+[[package]]
+name = "jiff"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba926fdd8e5b5e7f9700355b0831d8c416afe94b014b1023424037a187c9c582"
+dependencies = [
+ "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde",
+ "windows-sys",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf2cec2f5d266af45a071ece48b1fb89f3b00b2421ac3a5fe10285a6caaa60d3"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a63c62e404e7b92979d2792352d885a7f8f83fd1d0d31eea582d77b2ceca697e"
+dependencies = [
+ "jiff-tzdb",
+]
+
 [[package]]
 name = "jsonschema"
 version = "0.28.3"
@@ -585,6 +614,12 @@ version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
 
+[[package]]
+name = "log"
+version = "0.4.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
+
 [[package]]
 name = "memchr"
 version = "2.7.4"
@@ -678,9 +713,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.20.2"
+version = "1.20.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
 
 [[package]]
 name = "outref"
@@ -694,6 +729,21 @@ version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
+[[package]]
+name = "portable-atomic"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
+dependencies = [
+ "portable-atomic",
+]
+
 [[package]]
 name = "pretty_assertions"
 version = "1.4.1"
@@ -825,9 +875,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
 
 [[package]]
 name = "ryu"
-version = "1.0.18"
+version = "1.0.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
 
 [[package]]
 name = "schemars"
@@ -841,6 +891,7 @@ dependencies = [
  "either",
  "garde",
  "indexmap",
+ "jiff",
  "jsonschema",
  "pretty_assertions",
  "ref-cast",
@@ -913,9 +964,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.137"
+version = "1.0.138"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
+checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
 dependencies = [
  "indexmap",
  "itoa",
@@ -1012,9 +1063,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
 [[package]]
 name = "syn"
-version = "2.0.96"
+version = "2.0.98"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
+checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1059,9 +1110,9 @@ dependencies = [
 
 [[package]]
 name = "toml"
-version = "0.8.19"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
+checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
 dependencies = [
  "serde",
  "serde_spanned",
@@ -1080,9 +1131,9 @@ dependencies = [
 
 [[package]]
 name = "toml_edit"
-version = "0.22.22"
+version = "0.22.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
+checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
 dependencies = [
  "indexmap",
  "serde",
@@ -1108,9 +1159,9 @@ dependencies = [
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.15"
+version = "1.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
+checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
 
 [[package]]
 name = "url"
@@ -1144,9 +1195,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
 name = "uuid"
-version = "1.12.1"
+version = "1.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
+checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
 dependencies = [
  "serde",
 ]
@@ -1294,9 +1345,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
 name = "winnow"
-version = "0.6.24"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
+checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f"
 dependencies = [
  "memchr",
 ]
diff --git a/README.md b/README.md
index fc68efc..c441b22 100644
--- a/README.md
+++ b/README.md
@@ -269,6 +269,7 @@ Schemars can implement `JsonSchema` on types from several popular crates, enable
 - `chrono04` - [chrono](https://crates.io/crates/chrono) (^0.4)
 - `either1` - [either](https://crates.io/crates/either) (^1.3)
 - `indexmap2` - [indexmap](https://crates.io/crates/indexmap) (^2.0)
+- `jiff02` - [jiff](https://crates.io/crates/jiff) (^0.2)
 - `rust_decimal1` - [rust_decimal](https://crates.io/crates/rust_decimal) (^1.0)
 - `semver1` - [semver](https://crates.io/crates/semver) (^1.0.9)
 - `smallvec1` - [smallvec](https://crates.io/crates/smallvec) (^1.0)
diff --git a/docs/4-features.md b/docs/4-features.md
index 1141154..246dbc2 100644
--- a/docs/4-features.md
+++ b/docs/4-features.md
@@ -19,6 +19,7 @@ Schemars can implement `JsonSchema` on types from several popular crates, enable
 - `chrono04` - [chrono](https://crates.io/crates/chrono) (^0.4)
 - `either1` - [either](https://crates.io/crates/either) (^1.3)
 - `indexmap2` - [indexmap](https://crates.io/crates/indexmap) (^2.0)
+- `jiff02` - [jiff](https://crates.io/crates/jiff) (^0.2)
 - `rust_decimal1` - [rust_decimal](https://crates.io/crates/rust_decimal) (^1.0)
 - `semver1` - [semver](https://crates.io/crates/semver) (^1.0.9)
 - `smallvec1` - [smallvec](https://crates.io/crates/smallvec) (^1.0)
diff --git a/docs/_includes/example_v0.md b/docs/_includes/example_v0.md
index 8868524..eef7724 100644
--- a/docs/_includes/example_v0.md
+++ b/docs/_includes/example_v0.md
@@ -1,5 +1,5 @@
-{% capture input %}examples/{{ include.name }}.rs{% endcapture %}
-{% capture output %}examples/{{ include.name }}.schema.json{% endcapture %}
+{% capture input %}examples_v0/{{ include.name }}.rs{% endcapture %}
+{% capture output %}examples_v0/{{ include.name }}.schema.json{% endcapture %}
 
 ```rust
 {% include {{ input }} %}
@@ -11,4 +11,5 @@
 ```json
 {% include {{ output }} -%}
 ```
+
 </details>
diff --git a/schemars/Cargo.toml b/schemars/Cargo.toml
index 49b7092..c0e04ca 100644
--- a/schemars/Cargo.toml
+++ b/schemars/Cargo.toml
@@ -26,6 +26,7 @@ bytes1 = { version = "1.0", default-features = false, optional = true, package =
 chrono04 = { version = "0.4", default-features = false, optional = true, package = "chrono" }
 either1 = { version = "1.3", default-features = false, optional = true, package = "either" }
 indexmap2 = { version = "2.0", default-features = false, optional = true, package = "indexmap" }
+jiff02 = { version = "0.2", default-features = false, optional = true, package = "jiff" }
 rust_decimal1 = { version = "1", default-features = false, optional = true, package = "rust_decimal" }
 semver1 = { version = "1.0.9", default-features = false, optional = true, package = "semver" }
 smallvec1 = { version = "1.0", default-features = false, optional = true, package = "smallvec" }
@@ -50,6 +51,7 @@ bytes1 = { version = "1.0", default-features = false, features = ["serde"], pack
 chrono04 = { version = "0.4", default-features = false, features = ["serde"], package = "chrono" }
 either1 = { version = "1.3", default-features = false, features = ["serde"], package = "either" }
 indexmap2 = { version = "2.0", default-features = false, features = ["serde"], package = "indexmap" }
+jiff02 = { version = "0.2", features = ["serde"], package = "jiff" }
 rust_decimal1 = { version = "1", default-features = false, features = ["serde"], package = "rust_decimal" }
 semver1 = { version = "1.0.9", default-features = false, features = ["serde"], package = "semver" }
 smallvec1 = { version = "1.0", default-features = false, features = ["serde"], package = "smallvec" }
diff --git a/schemars/src/_private/mod.rs b/schemars/src/_private/mod.rs
index aa5c6fe..baf90f9 100644
--- a/schemars/src/_private/mod.rs
+++ b/schemars/src/_private/mod.rs
@@ -279,7 +279,7 @@ pub fn flatten(schema: &mut Schema, other: Schema) {
                     _ => {
                         // leave the original value as it is (don't modify `schema`)
                     }
-                };
+                }
             }
         }
     }
diff --git a/schemars/src/json_schema_impls/core.rs b/schemars/src/json_schema_impls/core.rs
index 07fbce5..489c6f4 100644
--- a/schemars/src/json_schema_impls/core.rs
+++ b/schemars/src/json_schema_impls/core.rs
@@ -57,7 +57,7 @@ impl<T: JsonSchema> JsonSchema for Option<T> {
             schema
                 .ensure_object()
                 .insert("nullable".into(), true.into());
-        };
+        }
 
         schema
     }
diff --git a/schemars/src/json_schema_impls/jiff02.rs b/schemars/src/json_schema_impls/jiff02.rs
new file mode 100644
index 0000000..1b43fbc
--- /dev/null
+++ b/schemars/src/json_schema_impls/jiff02.rs
@@ -0,0 +1,38 @@
+use crate::{json_schema, JsonSchema, Schema, SchemaGenerator};
+use alloc::borrow::Cow;
+use jiff02::civil::{Date, DateTime, Time};
+use jiff02::{SignedDuration, Span, Timestamp, Zoned};
+
+macro_rules! formatted_string_impl {
+    ($ty:ident, $format:literal) => {
+        formatted_string_impl!($ty, $format, JsonSchema for $ty);
+    };
+    ($ty:ident, $format:literal, $($desc:tt)+) => {
+        impl $($desc)+ {
+            always_inline!();
+
+            fn schema_name() -> Cow<'static, str> {
+                stringify!($ty).into()
+            }
+
+            fn schema_id() -> Cow<'static, str>  {
+                stringify!(jiff::$ty).into()
+            }
+
+            fn json_schema(_: &mut SchemaGenerator) -> Schema {
+                json_schema!({
+                    "type": "string",
+                    "format": $format
+                })
+            }
+        }
+    };
+}
+
+formatted_string_impl!(SignedDuration, "duration");
+formatted_string_impl!(Span, "duration");
+formatted_string_impl!(Timestamp, "date-time");
+formatted_string_impl!(Zoned, "zoned-date-time");
+formatted_string_impl!(Date, "date");
+formatted_string_impl!(Time, "partial-time");
+formatted_string_impl!(DateTime, "partial-date-time");
diff --git a/schemars/src/json_schema_impls/mod.rs b/schemars/src/json_schema_impls/mod.rs
index cbe1a07..089620b 100644
--- a/schemars/src/json_schema_impls/mod.rs
+++ b/schemars/src/json_schema_impls/mod.rs
@@ -74,6 +74,9 @@ mod either1;
 #[cfg(feature = "indexmap2")]
 mod indexmap2;
 
+#[cfg(feature = "jiff02")]
+mod jiff02;
+
 #[cfg(feature = "semver1")]
 mod semver1;
 
diff --git a/schemars/src/ser.rs b/schemars/src/ser.rs
index 079c566..1227af0 100644
--- a/schemars/src/ser.rs
+++ b/schemars/src/ser.rs
@@ -167,7 +167,7 @@ impl<'a> serde::Serializer for Serializer<'a> {
             schema
                 .ensure_object()
                 .insert("nullable".into(), true.into());
-        };
+        }
 
         Ok(schema)
     }


Test patch:
diff --git a/schemars/tests/integration/garde.rs b/schemars/tests/integration/garde.rs
index e7ca2a5..54c3786 100644
--- a/schemars/tests/integration/garde.rs
+++ b/schemars/tests/integration/garde.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::incompatible_msrv)]
+
 use crate::prelude::*;
 use garde::Validate;
 
diff --git a/schemars/tests/integration/jiff.rs b/schemars/tests/integration/jiff.rs
new file mode 100644
index 0000000..441dd5e
--- /dev/null
+++ b/schemars/tests/integration/jiff.rs
@@ -0,0 +1,62 @@
+use crate::prelude::*;
+use jiff02::civil::{Date, DateTime, Time};
+use jiff02::{SignedDuration, Span, Timestamp, Zoned};
+
+#[derive(JsonSchema, Serialize, Deserialize)]
+struct JiffTypes {
+    date_time_ts: Timestamp,
+    date_time_zoned: Zoned,
+    naive_date: Date,
+    naive_date_time: DateTime,
+    naive_time: Time,
+    duration: SignedDuration,
+    span: Span,
+}
+
+#[test]
+fn jiff() {
+    test!(JiffTypes).assert_snapshot();
+
+    test!(Timestamp)
+        .assert_allows_ser_roundtrip_default()
+        .assert_matches_de_roundtrip(arbitrary_values());
+
+    test!(Zoned)
+        .assert_allows_ser_roundtrip_default()
+        .assert_matches_de_roundtrip(arbitrary_values_except(
+            Value::is_string,
+            "Custom format 'zoned-date-time', so arbitrary strings technically allowed by schema",
+        ));
+
+    test!(Date)
+        .assert_allows_ser_roundtrip_default()
+        .assert_matches_de_roundtrip(arbitrary_values());
+
+    test!(DateTime)
+        .assert_allows_ser_roundtrip_default()
+        .assert_matches_de_roundtrip(arbitrary_values_except(
+            Value::is_string,
+            "Custom format 'partial-date-time', so arbitrary strings technically allowed by schema",
+        ));
+
+    test!(Time)
+        .assert_allows_ser_roundtrip_default()
+        .assert_matches_de_roundtrip(arbitrary_values_except(
+            Value::is_string,
+            "Custom format 'date-time', so arbitrary strings technically allowed by schema",
+        ));
+
+    test!(SignedDuration)
+        .assert_allows_ser_roundtrip_default()
+        .assert_matches_de_roundtrip(arbitrary_values_except(
+            Value::is_string,
+            "jsonschema 0.20 does not handle durations correctly - this can be removed once MSRV is increased",
+        ));
+
+    test!(Span)
+        .assert_allows_ser_roundtrip_default()
+        .assert_matches_de_roundtrip(arbitrary_values_except(
+            Value::is_string,
+            "jsonschema 0.20 does not handle durations correctly - this can be removed once MSRV is increased",
+        ));
+}
diff --git a/schemars/tests/integration/main.rs b/schemars/tests/integration/main.rs
index eb44526..0cbacb9 100644
--- a/schemars/tests/integration/main.rs
+++ b/schemars/tests/integration/main.rs
@@ -28,6 +28,8 @@ mod garde;
 #[cfg(feature = "indexmap2")]
 mod indexmap;
 mod inline_subschemas;
+#[cfg(feature = "jiff02")]
+mod jiff;
 mod macros;
 mod remote_derive;
 mod same_name;
diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/jiff.rs~jiff.json b/schemars/tests/integration/snapshots/schemars/tests/integration/jiff.rs~jiff.json
new file mode 100644
index 0000000..b7c89d4
--- /dev/null
+++ b/schemars/tests/integration/snapshots/schemars/tests/integration/jiff.rs~jiff.json
@@ -0,0 +1,44 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "title": "JiffTypes",
+  "type": "object",
+  "properties": {
+    "date_time_ts": {
+      "type": "string",
+      "format": "date-time"
+    },
+    "date_time_zoned": {
+      "type": "string",
+      "format": "zoned-date-time"
+    },
+    "naive_date": {
+      "type": "string",
+      "format": "date"
+    },
+    "naive_date_time": {
+      "type": "string",
+      "format": "partial-date-time"
+    },
+    "naive_time": {
+      "type": "string",
+      "format": "partial-time"
+    },
+    "duration": {
+      "type": "string",
+      "format": "duration"
+    },
+    "span": {
+      "type": "string",
+      "format": "duration"
+    }
+  },
+  "required": [
+    "date_time_ts",
+    "date_time_zoned",
+    "naive_date",
+    "naive_date_time",
+    "naive_time",
+    "duration",
+    "span"
+  ]
+}
\ No newline at end of file