json_schema_rs/reverse_code_gen/
mod.rs1use crate::json_schema::JsonSchema;
7use crate::json_schema::json_schema::AdditionalProperties;
8use std::collections::BTreeMap;
9
10pub trait ToJsonSchema {
15 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
160pub 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#[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}