Skip to main content

json_schema_derive/
lib.rs

1//! Derive macro for generating JSON Schema from Rust types.
2//!
3//! This crate provides a `#[derive(JsonSchema)]` macro that generates a JSON Schema
4//! for your types. It supports custom schema attributes via `#[json_schema(...)]`
5//! and optionally integrates with common `serde` attributes when the `serde-compat` feature is enabled.
6//!
7//! # Example
8//! ```rust
9//! use json_schema_derive::JsonSchema;
10//!
11//! #[derive(JsonSchema)]
12//! struct User {
13//!     #[json_schema(comment = "User's name", minLength = 2)]
14//!     name: String,
15//!     /// User's age
16//!     age: u32,
17//!     tags: Vec<String>,
18//! }
19//!
20//! let schema = User::json_schema();
21//! ```
22//!
23//! # Features
24//!
25//! - `serde-compat`: Enables compatibility with serde attributes for schema generation
26//! # Serde Compatibility
27//!
28//! When the `serde-compat` feature is enabled, the following `serde` attributes are supported:
29//!
30//! - `#[serde(skip)]` – Omits the field from the schema  
31//! - `#[serde(rename = "new_name")]` – Renames the field in the schema  
32//! - `#[serde(flatten)]` – Inlines nested struct fields  
33//! - `#[serde(tag = "...")]` – Supports internally tagged enums
34//!
35//! ```rust
36//! #[derive(JsonSchema)]
37//! #[serde(tag = "type")]
38//! enum Event {
39//!     Login { user: String },
40//!     Logout,
41//! }
42//! ```
43
44use core::str;
45
46pub use json_schema_derive_macro::JsonSchema;
47// mod expanded;
48
49/// Trait for generating JSON Schema from a type.
50///
51/// This trait is automatically implemented for types that derive `JsonSchema`.
52/// It provides a method to generate a JSON Schema representation of the type.
53pub trait JsonSchema {
54    /// Generate a JSON Schema representation of the type.
55    ///
56    /// Returns a `serde_json::Value` containing the JSON Schema.
57    fn json_schema() -> serde_json::Value;
58}
59
60macro_rules! impl_json_schema {
61    ($name:expr, $($t:ty),*) => {
62        $(
63            impl JsonSchema for $t {
64                fn json_schema() -> serde_json::Value {
65                    serde_json::json!({ "type": $name })
66                }
67            }
68        )*
69    };
70}
71
72impl_json_schema!("number", u8, u16, u32, u64, i8, i16, i32, i64, f32, f64);
73impl_json_schema!("boolean", bool);
74impl_json_schema!("string", String, &str);
75
76impl JsonSchema for () {
77    fn json_schema() -> serde_json::Value {
78        serde_json::json!({ "type": "null" })
79    }
80}
81
82impl<T: JsonSchema> JsonSchema for Vec<T> {
83    fn json_schema() -> serde_json::Value {
84        serde_json::json!({ "type": "array", "items": T::json_schema() })
85    }
86}
87
88impl<T: JsonSchema, const N: usize> JsonSchema for [T; N] {
89    fn json_schema() -> serde_json::Value {
90        serde_json::json!({ "type": "array", "items": T::json_schema(), "maxItems": N, "minItems": N })
91    }
92}
93
94impl<T: JsonSchema> JsonSchema for Option<T> {
95    fn json_schema() -> serde_json::Value {
96        T::json_schema()
97    }
98}
99
100impl<T: JsonSchema> JsonSchema for &Option<T> {
101    fn json_schema() -> serde_json::Value {
102        T::json_schema()
103    }
104}
105
106impl<T: JsonSchema> JsonSchema for Box<T> {
107    fn json_schema() -> serde_json::Value {
108        T::json_schema()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use serde::Serialize;
116    use serde_json::json;
117
118    pub fn valid<T: JsonSchema + Serialize>(instance: &T) -> bool {
119        let schema = T::json_schema();
120        let json = serde_json::to_value(instance).unwrap();
121        jsonschema::is_valid(&schema, &json)
122    }
123
124    #[test]
125    fn test_impl_json_schema() {
126        assert_eq!(u32::json_schema(), json!({ "type": "number" }));
127        assert_eq!(bool::json_schema(), json!({ "type": "boolean" }));
128        assert_eq!(String::json_schema(), json!({ "type": "string" }));
129        assert_eq!(
130            <Vec<u32>>::json_schema(),
131            json!({ "type": "array", "items": { "type": "number" } })
132        );
133        assert_eq!(<Option<bool>>::json_schema(), json!({ "type": "boolean" }));
134        assert_eq!(
135            <[u32; 3]>::json_schema(),
136            json!({ "type": "array", "items": { "type": "number" }, "maxItems": 3, "minItems": 3 })
137        );
138
139        assert!(valid::<u32>(&10));
140        assert!(valid::<bool>(&true));
141        assert!(valid::<String>(&"test".to_string()));
142        assert!(valid::<Vec<u32>>(&vec![1, 2, 3]));
143        assert!(valid::<Option<bool>>(&Some(true)));
144        assert!(valid::<[u32; 3]>(&[1, 2, 3]));
145    }
146
147    #[derive(JsonSchema, Serialize)]
148    #[json_schema(comment = "Test comment")]
149    #[allow(dead_code)]
150    struct TestStruct {
151        #[json_schema(comment = "test field", minLength = 3)]
152        name: String,
153        age: u32,
154        active: Option<bool>,
155        scores: Vec<i32>,
156    }
157
158    #[test]
159    fn test_struct_schema() {
160        let schema = TestStruct::json_schema();
161        let expected = json!({
162            "type": "object",
163            "properties": {
164                "name": {
165                    "type": "string",
166                    "comment": "test field",
167                    "minLength": 3
168                },
169                "age": {
170                    "type": "number"
171                },
172                "active": {
173                    "type": "boolean"
174                },
175                "scores": {
176                    "type": "array",
177                    "items": {"type": "number"}
178                }
179            },
180            "required": ["name", "age", "scores"],
181            "comment": "Test comment"
182        });
183        assert_eq!(schema, expected);
184        assert!(valid(&TestStruct {
185            name: "test".to_string(),
186            age: 10,
187            active: Some(true),
188            scores: vec![1, 2, 3],
189        }));
190    }
191
192    #[derive(JsonSchema)]
193    #[allow(dead_code)]
194    struct NestedStruct {
195        inner: Option<TestStruct>,
196        tags: Option<Vec<String>>,
197    }
198
199    #[test]
200    fn test_nested_struct() {
201        let schema = NestedStruct::json_schema();
202        let expected = json!({
203            "type": "object",
204            "properties": {
205                "inner": {
206                    "type": "object",
207                    "properties": {
208                        "name": {
209                            "type": "string",
210                            "comment": "test field",
211                            "minLength": 3
212                        },
213                        "age": {
214                            "type": "number"
215                        },
216                        "active": {
217                            "type": "boolean"
218                        },
219                        "scores": {
220                            "type": "array",
221                            "items": {"type": "number"}
222                        }
223                    },
224                    "required": ["name", "age", "scores"],
225                    "comment": "Test comment"
226                },
227                "tags": {
228                    "type": "array",
229                    "items": {"type": "string"}
230                }
231            },
232            "required": []
233        });
234        assert_eq!(schema, expected);
235    }
236
237    #[derive(JsonSchema, Serialize)]
238    #[json_schema(comment = "Test comment")]
239    #[allow(dead_code)]
240    struct TestStructUnnamed(String);
241
242    #[test]
243    fn test_struct_unnamed() {
244        let schema = TestStructUnnamed::json_schema();
245        let expected = json!({ "comment": "Test comment", "type": "string" });
246        assert_eq!(schema, expected);
247        assert!(valid(&TestStructUnnamed("test".to_string())));
248    }
249
250    #[derive(JsonSchema, Serialize)]
251    #[json_schema(comment = "Test comment")]
252    #[allow(dead_code)]
253    struct TestStructUnnamedMultiple(String, u32);
254
255    #[test]
256    fn test_struct_unnamed_multiple() {
257        let schema = TestStructUnnamedMultiple::json_schema();
258        let expected = json!({
259            "comment": "Test comment",
260            "type": "array",
261            "prefixItems": [{ "type": "string" }, { "type": "number" }],
262            "minItems": 2,
263            "maxItems": 2,
264            "unevaluatedItems": false,
265        });
266        assert_eq!(schema, expected);
267        assert!(valid(&TestStructUnnamedMultiple("test".to_string(), 10)));
268    }
269
270    #[derive(JsonSchema, Serialize)]
271    #[json_schema(comment = "Test comment")]
272    #[allow(dead_code)]
273    enum EnumUnit {
274        A,
275        B,
276        C,
277    }
278
279    #[test]
280    fn test_enum_unit() {
281        let schema = EnumUnit::json_schema();
282        let expected = json!({
283            "type": "string",
284            "comment": "Test comment",
285            "enum": ["A", "B", "C"],
286        });
287        println!("{:#?}", serde_json::to_value(&EnumUnit::A).unwrap());
288        assert_eq!(schema, expected);
289        assert!(valid(&EnumUnit::A));
290        assert!(valid(&EnumUnit::B));
291        assert!(valid(&EnumUnit::C));
292    }
293
294    #[derive(JsonSchema, Serialize)]
295    #[json_schema(comment = "Test comment")]
296    #[allow(dead_code)]
297    enum EnumUnnamed {
298        A(String),
299        B(u32),
300    }
301
302    #[test]
303    fn test_enum_unit_unnamed() {
304        let schema = EnumUnnamed::json_schema();
305        let expected = json!({
306            "type": "object",
307            "comment": "Test comment",
308            "properties": {
309                "A": { "type": "string" },
310                "B": { "type": "number" },
311            }
312        });
313        assert_eq!(schema, expected);
314        assert!(valid(&EnumUnnamed::A("test".to_string())));
315        assert!(valid(&EnumUnnamed::B(10)));
316    }
317
318    #[derive(JsonSchema, Serialize)]
319    #[json_schema(comment = "Test comment")]
320    #[allow(dead_code)]
321    enum EnumNamed {
322        A { name: String },
323        B { age: u32 },
324    }
325
326    #[test]
327    fn test_enum_named() {
328        let schema = EnumNamed::json_schema();
329        let expected = json!({
330            "type": "object",
331            "comment": "Test comment",
332            "properties": {
333                "A": { "type": "object", "properties": { "name": { "type": "string" } }, "required": ["name"] },
334                "B": { "type": "object", "properties": { "age": { "type": "number" } }, "required": ["age"] },
335            }
336        });
337        assert_eq!(schema, expected);
338        assert!(valid(&EnumNamed::A {
339            name: "test".to_string()
340        }));
341        assert!(valid(&EnumNamed::B { age: 10 }));
342    }
343
344    #[derive(JsonSchema, Serialize)]
345    #[allow(dead_code)]
346    /// Test description
347    struct TestStructDoc {
348        /// Test field description
349        name: String,
350    }
351
352    #[test]
353    fn test_struct_doc() {
354        let schema = TestStructDoc::json_schema();
355        let expected = json!({ "type": "object", "description": "Test description", "properties": { "name": { "type": "string", "description": "Test field description" } }, "required": ["name"] });
356        assert_eq!(schema, expected);
357        assert!(valid(&TestStructDoc {
358            name: "test".to_string()
359        }));
360    }
361}
362
363#[cfg(feature = "serde-compat")]
364#[cfg(test)]
365mod tests_serde_compat {
366    use super::*;
367    use serde::Serialize;
368    use serde_json::json;
369
370    #[derive(JsonSchema, Serialize)]
371    #[json_schema(comment = "Test comment")]
372    #[allow(dead_code)]
373    struct TestStructWithSerde {
374        #[serde(skip)]
375        skip: u32,
376        #[serde(rename = "foo")]
377        renamed: u32,
378    }
379
380    #[test]
381    fn test_struct_with_serde() {
382        let schema = TestStructWithSerde::json_schema();
383        let expected = json!({
384            "type": "object",
385            "properties": { "foo": { "type": "number" } },
386            "required": ["foo"],
387            "comment": "Test comment"
388        });
389        assert_eq!(schema, expected);
390        assert!(tests::valid(&TestStructWithSerde {
391            skip: 0,
392            renamed: 10,
393        }));
394    }
395
396    #[derive(JsonSchema, Serialize)]
397    #[json_schema(comment = "Test comment")]
398    #[allow(dead_code)]
399    struct TestStructWithFlatten {
400        #[serde(flatten)]
401        inner: TestStructWithSerde,
402    }
403
404    #[test]
405    fn test_struct_with_flatten() {
406        let schema = TestStructWithFlatten::json_schema();
407        let expected = json!({
408            "type": "object",
409            "properties": { "foo": { "type": "number" } },
410            "required": ["foo"],
411            "comment": "Test comment"
412        });
413        println!("{:#?}", schema);
414        assert_eq!(schema, expected);
415        assert!(tests::valid(&TestStructWithFlatten {
416            inner: TestStructWithSerde {
417                skip: 0,
418                renamed: 10,
419            }
420        }));
421    }
422
423    #[derive(JsonSchema, Serialize)]
424    #[allow(dead_code)]
425    #[serde(tag = "type")]
426    enum EnumUnitSerdeTag {
427        A,
428        B,
429    }
430
431    #[test]
432    fn test_enum_serde_tag() {
433        let schema = EnumUnitSerdeTag::json_schema();
434        let expected = json!({
435            "oneOf": [
436                { "type": "object", "properties": { "type": { "type": "string", "const": "A" } }, "required": ["type"] },
437                { "type": "object", "properties": { "type": { "type": "string", "const": "B" } }, "required": ["type"] }
438            ]
439        });
440        assert_eq!(schema, expected);
441        assert!(tests::valid(&EnumUnitSerdeTag::A));
442        assert!(tests::valid(&EnumUnitSerdeTag::B));
443    }
444
445    #[derive(JsonSchema, Serialize)]
446    #[allow(dead_code)]
447    #[serde(tag = "type")]
448    enum EnumNamedSerdeTag {
449        A { name: String },
450        B { age: u32 },
451        C,
452    }
453
454    #[test]
455    fn test_enum_named_serde_tag() {
456        let schema = EnumNamedSerdeTag::json_schema();
457        let expected = json!({
458            "oneOf": [
459                { "type": "object", "properties": { "type": { "type": "string", "const": "A" }, "name": { "type": "string" } }, "required": ["name", "type"] },
460                { "type": "object", "properties": { "type": { "type": "string", "const": "B" }, "age": { "type": "number" } }, "required": ["age", "type"] },
461                { "type": "object", "properties": { "type": { "type": "string", "const": "C" } }, "required": ["type"] }
462            ]
463        });
464        assert_eq!(schema, expected);
465        assert!(tests::valid(&EnumNamedSerdeTag::A {
466            name: "test".to_string()
467        }));
468        assert!(tests::valid(&EnumNamedSerdeTag::B { age: 10 }));
469    }
470}