Skip to main content

json_schema_rs/reverse_code_gen/
mod.rs

1//! Reverse codegen: Rust types to JSON Schema.
2//!
3//! Types implement [`ToJsonSchema`] to produce a [`JsonSchema`] value that can be
4//! serialized via [`TryFrom`] to String or `Vec<u8>`.
5
6use crate::json_schema::JsonSchema;
7use crate::json_schema::json_schema::AdditionalProperties;
8use std::collections::BTreeMap;
9
10/// Trait for types that can produce a JSON Schema.
11///
12/// Implemented for primitive/standard types (e.g. `String`, `Option<T>`) and for
13/// structs via `#[derive(ToJsonSchema)]` with optional container/field attributes.
14pub trait ToJsonSchema {
15    /// Returns the JSON Schema for this type.
16    fn json_schema() -> JsonSchema;
17}
18
19impl ToJsonSchema for String {
20    fn json_schema() -> JsonSchema {
21        JsonSchema {
22            type_: Some("string".to_string()),
23            ..Default::default()
24        }
25    }
26}
27
28impl ToJsonSchema for bool {
29    fn json_schema() -> JsonSchema {
30        JsonSchema {
31            type_: Some("boolean".to_string()),
32            ..Default::default()
33        }
34    }
35}
36
37fn integer_schema_with_bounds(min: f64, max: f64) -> JsonSchema {
38    JsonSchema {
39        type_: Some("integer".to_string()),
40        minimum: Some(min),
41        maximum: Some(max),
42        ..Default::default()
43    }
44}
45
46impl ToJsonSchema for i8 {
47    fn json_schema() -> JsonSchema {
48        integer_schema_with_bounds(f64::from(i8::MIN), f64::from(i8::MAX))
49    }
50}
51
52impl ToJsonSchema for u8 {
53    fn json_schema() -> JsonSchema {
54        integer_schema_with_bounds(f64::from(u8::MIN), f64::from(u8::MAX))
55    }
56}
57
58impl ToJsonSchema for i16 {
59    fn json_schema() -> JsonSchema {
60        integer_schema_with_bounds(f64::from(i16::MIN), f64::from(i16::MAX))
61    }
62}
63
64impl ToJsonSchema for u16 {
65    fn json_schema() -> JsonSchema {
66        integer_schema_with_bounds(f64::from(u16::MIN), f64::from(u16::MAX))
67    }
68}
69
70impl ToJsonSchema for i32 {
71    fn json_schema() -> JsonSchema {
72        integer_schema_with_bounds(f64::from(i32::MIN), f64::from(i32::MAX))
73    }
74}
75
76impl ToJsonSchema for u32 {
77    fn json_schema() -> JsonSchema {
78        integer_schema_with_bounds(f64::from(u32::MIN), f64::from(u32::MAX))
79    }
80}
81
82impl ToJsonSchema for i64 {
83    fn json_schema() -> JsonSchema {
84        #[expect(clippy::cast_precision_loss)]
85        integer_schema_with_bounds(i64::MIN as f64, 9_223_372_036_854_775_807.0_f64)
86    }
87}
88
89impl ToJsonSchema for u64 {
90    fn json_schema() -> JsonSchema {
91        integer_schema_with_bounds(0.0_f64, 18_446_744_073_709_551_615.0_f64)
92    }
93}
94
95fn number_schema_with_bounds(min: f64, max: f64) -> JsonSchema {
96    JsonSchema {
97        type_: Some("number".to_string()),
98        minimum: Some(min),
99        maximum: Some(max),
100        ..Default::default()
101    }
102}
103
104impl ToJsonSchema for f32 {
105    fn json_schema() -> JsonSchema {
106        number_schema_with_bounds(f64::from(f32::MIN), f64::from(f32::MAX))
107    }
108}
109
110impl ToJsonSchema for f64 {
111    fn json_schema() -> JsonSchema {
112        number_schema_with_bounds(f64::MIN, f64::MAX)
113    }
114}
115
116impl<T: ToJsonSchema> ToJsonSchema for Option<T> {
117    fn json_schema() -> JsonSchema {
118        T::json_schema()
119    }
120}
121
122impl<T: ToJsonSchema> ToJsonSchema for Vec<T> {
123    fn json_schema() -> JsonSchema {
124        JsonSchema {
125            type_: Some("array".to_string()),
126            items: Some(Box::new(T::json_schema())),
127            ..Default::default()
128        }
129    }
130}
131
132#[expect(clippy::implicit_hasher)]
133impl<T: ToJsonSchema + std::hash::Hash + Eq> ToJsonSchema for std::collections::HashSet<T> {
134    fn json_schema() -> JsonSchema {
135        JsonSchema {
136            type_: Some("array".to_string()),
137            items: Some(Box::new(T::json_schema())),
138            unique_items: Some(true),
139            ..Default::default()
140        }
141    }
142}
143
144impl<V: ToJsonSchema> ToJsonSchema for BTreeMap<String, V> {
145    fn json_schema() -> JsonSchema {
146        JsonSchema {
147            type_: Some("object".to_string()),
148            additional_properties: Some(AdditionalProperties::Schema(Box::new(V::json_schema()))),
149            ..Default::default()
150        }
151    }
152}
153
154impl<T: ToJsonSchema> ToJsonSchema for Box<T> {
155    fn json_schema() -> JsonSchema {
156        T::json_schema()
157    }
158}
159
160/// Merges nested `$defs` from `schema` into `root_defs`, returns schema with `defs: None`.
161///
162/// Recursively flattens so the final result has a single root-level defs map.
163/// Uses an explicit stack (no recursion) per the "no literal recursion" design principle.
164///
165/// When a schema has `defs: { Outer: { ..., defs: { Inner } } }`, this function merges
166/// `Inner` and `Outer` into `root_defs` and returns the schema with `defs: None`.
167/// The `$ref` values in properties/items already point to `#/$defs/Name` and will
168/// resolve against the root.
169///
170/// # Panics
171///
172/// Never panics. The `unwrap` on `defs.take()` is safe because we only reach that
173/// branch when `schema.defs` is `Some` and non-empty.
174pub fn merge_nested_defs_into_root(
175    schema: JsonSchema,
176    root_defs: &mut BTreeMap<String, JsonSchema>,
177) -> JsonSchema {
178    let mut stack: Vec<(Option<String>, JsonSchema)> = Vec::new();
179    stack.push((None, schema));
180
181    let mut result: Option<JsonSchema> = None;
182
183    while let Some((key_opt, mut s)) = stack.pop() {
184        let has_nested_defs: bool = s.defs.as_ref().is_some_and(|m| !m.is_empty());
185
186        if has_nested_defs {
187            let defs: BTreeMap<String, JsonSchema> = s.defs.take().unwrap();
188            stack.push((key_opt, s));
189            for (k, v) in defs.into_iter().rev() {
190                stack.push((Some(k), v));
191            }
192        } else if let Some(k) = key_opt {
193            root_defs.entry(k).or_insert(s);
194        } else {
195            result = Some(s);
196        }
197    }
198
199    result.expect("root schema must have been processed")
200}
201
202/// Minimal hand-written struct implementing [`ToJsonSchema`] (used to validate trait shape).
203#[derive(Debug, Clone)]
204pub struct HandWrittenExample;
205
206impl ToJsonSchema for HandWrittenExample {
207    fn json_schema() -> JsonSchema {
208        JsonSchema {
209            type_: Some("object".to_string()),
210            title: Some("HandWrittenExample".to_string()),
211            ..Default::default()
212        }
213    }
214}
215
216#[cfg(feature = "uuid")]
217impl ToJsonSchema for uuid::Uuid {
218    fn json_schema() -> JsonSchema {
219        JsonSchema {
220            type_: Some("string".to_string()),
221            format: Some("uuid".to_string()),
222            ..Default::default()
223        }
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::ToJsonSchema;
230    use crate::json_schema::JsonSchema;
231    use std::collections::BTreeMap;
232
233    #[test]
234    fn string_json_schema() {
235        let expected: JsonSchema = JsonSchema {
236            type_: Some("string".to_string()),
237            ..Default::default()
238        };
239        let actual: JsonSchema = String::json_schema();
240        assert_eq!(expected, actual);
241    }
242
243    #[test]
244    fn bool_json_schema() {
245        let expected: JsonSchema = JsonSchema {
246            type_: Some("boolean".to_string()),
247            ..Default::default()
248        };
249        let actual: JsonSchema = bool::json_schema();
250        assert_eq!(expected, actual);
251    }
252
253    #[test]
254    fn option_string_json_schema() {
255        let expected: JsonSchema = String::json_schema();
256        let actual: JsonSchema = Option::<String>::json_schema();
257        assert_eq!(expected, actual);
258    }
259
260    #[test]
261    fn i64_json_schema() {
262        let expected: JsonSchema = JsonSchema {
263            type_: Some("integer".to_string()),
264            #[expect(clippy::cast_precision_loss)]
265            minimum: Some(i64::MIN as f64),
266            maximum: Some(9_223_372_036_854_775_807.0_f64),
267            ..Default::default()
268        };
269        let actual: JsonSchema = i64::json_schema();
270        assert_eq!(expected, actual);
271    }
272
273    #[test]
274    fn option_i64_json_schema() {
275        let expected: JsonSchema = i64::json_schema();
276        let actual: JsonSchema = Option::<i64>::json_schema();
277        assert_eq!(expected, actual);
278    }
279
280    #[test]
281    fn i32_json_schema() {
282        let expected_type: Option<&str> = Some("integer");
283        let actual: JsonSchema = i32::json_schema();
284        assert_eq!(expected_type, actual.type_.as_deref());
285        assert_eq!(Some(f64::from(i32::MIN)), actual.minimum);
286        assert_eq!(Some(f64::from(i32::MAX)), actual.maximum);
287    }
288
289    #[test]
290    fn u32_json_schema() {
291        let expected_type: Option<&str> = Some("integer");
292        let actual: JsonSchema = u32::json_schema();
293        assert_eq!(expected_type, actual.type_.as_deref());
294        assert_eq!(Some(f64::from(u32::MIN)), actual.minimum);
295        assert_eq!(Some(f64::from(u32::MAX)), actual.maximum);
296    }
297
298    #[test]
299    fn u64_json_schema() {
300        let expected_type: Option<&str> = Some("integer");
301        let actual: JsonSchema = u64::json_schema();
302        assert_eq!(expected_type, actual.type_.as_deref());
303        assert_eq!(Some(0.0_f64), actual.minimum);
304        assert_eq!(Some(18_446_744_073_709_551_615.0_f64), actual.maximum);
305    }
306
307    #[test]
308    fn i8_json_schema() {
309        let expected_type: Option<&str> = Some("integer");
310        let actual: JsonSchema = i8::json_schema();
311        assert_eq!(expected_type, actual.type_.as_deref());
312        assert_eq!(Some(f64::from(i8::MIN)), actual.minimum);
313        assert_eq!(Some(f64::from(i8::MAX)), actual.maximum);
314    }
315
316    #[test]
317    fn u8_json_schema() {
318        let expected_type: Option<&str> = Some("integer");
319        let actual: JsonSchema = u8::json_schema();
320        assert_eq!(expected_type, actual.type_.as_deref());
321        assert_eq!(Some(0.0_f64), actual.minimum);
322        assert_eq!(Some(255.0_f64), actual.maximum);
323    }
324
325    #[test]
326    fn i16_json_schema() {
327        let expected_type: Option<&str> = Some("integer");
328        let actual: JsonSchema = i16::json_schema();
329        assert_eq!(expected_type, actual.type_.as_deref());
330        assert_eq!(Some(f64::from(i16::MIN)), actual.minimum);
331        assert_eq!(Some(f64::from(i16::MAX)), actual.maximum);
332    }
333
334    #[test]
335    fn u16_json_schema() {
336        let expected_type: Option<&str> = Some("integer");
337        let actual: JsonSchema = u16::json_schema();
338        assert_eq!(expected_type, actual.type_.as_deref());
339        assert_eq!(Some(f64::from(u16::MIN)), actual.minimum);
340        assert_eq!(Some(f64::from(u16::MAX)), actual.maximum);
341    }
342
343    #[test]
344    fn f64_json_schema() {
345        let expected: JsonSchema = JsonSchema {
346            type_: Some("number".to_string()),
347            minimum: Some(f64::MIN),
348            maximum: Some(f64::MAX),
349            ..Default::default()
350        };
351        let actual: JsonSchema = f64::json_schema();
352        assert_eq!(expected, actual);
353    }
354
355    #[test]
356    fn option_f64_json_schema() {
357        let expected: JsonSchema = f64::json_schema();
358        let actual: JsonSchema = Option::<f64>::json_schema();
359        assert_eq!(expected, actual);
360    }
361
362    #[test]
363    fn f32_json_schema() {
364        let expected_type: Option<&str> = Some("number");
365        let actual: JsonSchema = f32::json_schema();
366        assert_eq!(expected_type, actual.type_.as_deref());
367        assert_eq!(Some(f64::from(f32::MIN)), actual.minimum);
368        assert_eq!(Some(f64::from(f32::MAX)), actual.maximum);
369    }
370
371    #[test]
372    fn hand_written_example_json_schema() {
373        let expected: JsonSchema = JsonSchema {
374            type_: Some("object".to_string()),
375            title: Some("HandWrittenExample".to_string()),
376            ..Default::default()
377        };
378        let actual: JsonSchema = super::HandWrittenExample::json_schema();
379        assert_eq!(expected, actual);
380    }
381
382    #[test]
383    fn vec_string_json_schema() {
384        let expected: JsonSchema = JsonSchema {
385            type_: Some("array".to_string()),
386            items: Some(Box::new(String::json_schema())),
387            ..Default::default()
388        };
389        let actual: JsonSchema = Vec::<String>::json_schema();
390        assert_eq!(expected, actual);
391    }
392
393    #[test]
394    fn vec_i64_json_schema() {
395        let expected: JsonSchema = JsonSchema {
396            type_: Some("array".to_string()),
397            items: Some(Box::new(i64::json_schema())),
398            ..Default::default()
399        };
400        let actual: JsonSchema = Vec::<i64>::json_schema();
401        assert_eq!(expected, actual);
402    }
403
404    #[test]
405    fn option_vec_string_json_schema() {
406        let expected: JsonSchema = Vec::<String>::json_schema();
407        let actual: JsonSchema = Option::<Vec<String>>::json_schema();
408        assert_eq!(expected, actual);
409    }
410
411    #[test]
412    fn hash_set_string_json_schema_has_unique_items_true() {
413        use std::collections::HashSet;
414        let actual: JsonSchema = HashSet::<String>::json_schema();
415        let expected_unique: Option<bool> = Some(true);
416        assert_eq!(expected_unique, actual.unique_items);
417        assert_eq!(actual.type_.as_deref(), Some("array"));
418        let items: &JsonSchema = actual.items.as_ref().expect("items").as_ref();
419        assert_eq!(items.type_.as_deref(), Some("string"));
420    }
421
422    #[test]
423    fn vec_string_json_schema_has_unique_items_none() {
424        let actual: JsonSchema = Vec::<String>::json_schema();
425        let expected_unique: Option<bool> = None;
426        assert_eq!(expected_unique, actual.unique_items);
427    }
428
429    #[cfg(feature = "uuid")]
430    #[test]
431    fn uuid_json_schema() {
432        let actual: JsonSchema = uuid::Uuid::json_schema();
433        assert_eq!(actual.type_.as_deref(), Some("string"));
434        assert_eq!(actual.format.as_deref(), Some("uuid"));
435    }
436
437    #[cfg(feature = "uuid")]
438    #[test]
439    fn option_uuid_json_schema() {
440        let actual: JsonSchema = Option::<uuid::Uuid>::json_schema();
441        assert_eq!(actual.type_.as_deref(), Some("string"));
442        assert_eq!(actual.format.as_deref(), Some("uuid"));
443    }
444
445    #[cfg(feature = "uuid")]
446    #[test]
447    fn vec_uuid_json_schema() {
448        let actual: JsonSchema = Vec::<uuid::Uuid>::json_schema();
449        assert_eq!(actual.type_.as_deref(), Some("array"));
450        let items: &JsonSchema = actual.items.as_ref().expect("items").as_ref();
451        assert_eq!(items.type_.as_deref(), Some("string"));
452        assert_eq!(items.format.as_deref(), Some("uuid"));
453    }
454
455    #[test]
456    fn merge_nested_defs_into_root_flattens() {
457        use super::merge_nested_defs_into_root;
458
459        let inner_schema: JsonSchema = JsonSchema {
460            type_: Some("string".to_string()),
461            ..Default::default()
462        };
463        let mut outer_defs: BTreeMap<String, JsonSchema> = BTreeMap::new();
464        outer_defs.insert("Inner".to_string(), inner_schema.clone());
465        let outer_schema: JsonSchema = JsonSchema {
466            type_: Some("object".to_string()),
467            properties: {
468                let mut m = BTreeMap::new();
469                m.insert(
470                    "b".to_string(),
471                    JsonSchema {
472                        ref_: Some("#/$defs/Inner".to_string()),
473                        ..Default::default()
474                    },
475                );
476                m
477            },
478            defs: Some(outer_defs),
479            ..Default::default()
480        };
481        let mut schema_defs: BTreeMap<String, JsonSchema> = BTreeMap::new();
482        schema_defs.insert("Outer".to_string(), outer_schema);
483        let schema_with_nested: JsonSchema = JsonSchema {
484            type_: Some("object".to_string()),
485            properties: {
486                let mut m = BTreeMap::new();
487                m.insert(
488                    "a".to_string(),
489                    JsonSchema {
490                        ref_: Some("#/$defs/Outer".to_string()),
491                        ..Default::default()
492                    },
493                );
494                m
495            },
496            defs: Some(schema_defs),
497            ..Default::default()
498        };
499
500        let mut root_defs: BTreeMap<String, JsonSchema> = BTreeMap::new();
501        let actual: JsonSchema = merge_nested_defs_into_root(schema_with_nested, &mut root_defs);
502
503        let expected_returned: JsonSchema = JsonSchema {
504            type_: Some("object".to_string()),
505            defs: None,
506            properties: {
507                let mut m = BTreeMap::new();
508                m.insert(
509                    "a".to_string(),
510                    JsonSchema {
511                        ref_: Some("#/$defs/Outer".to_string()),
512                        ..Default::default()
513                    },
514                );
515                m
516            },
517            ..Default::default()
518        };
519        let mut expected_root_defs: BTreeMap<String, JsonSchema> = BTreeMap::new();
520        expected_root_defs.insert("Inner".to_string(), inner_schema);
521        expected_root_defs.insert(
522            "Outer".to_string(),
523            JsonSchema {
524                type_: Some("object".to_string()),
525                defs: None,
526                properties: {
527                    let mut m = BTreeMap::new();
528                    m.insert(
529                        "b".to_string(),
530                        JsonSchema {
531                            ref_: Some("#/$defs/Inner".to_string()),
532                            ..Default::default()
533                        },
534                    );
535                    m
536                },
537                ..Default::default()
538            },
539        );
540
541        assert_eq!((expected_returned, expected_root_defs), (actual, root_defs));
542    }
543}