1use super::CodeGenBackend;
4use super::CodeGenError;
5use super::CodeGenResult;
6use super::GenerateRustOutput;
7use super::settings::{CodeGenSettings, DedupeMode, ModelNameSource};
8use crate::json_schema::JsonSchema;
9use crate::json_schema::json_schema::AdditionalProperties;
10use crate::json_schema::ref_resolver;
11use crate::sanitizers::{
12 enum_variant_names_with_collision_resolution, sanitize_field_name, sanitize_struct_name,
13};
14use heck::ToSnakeCase;
15use std::cmp::Ordering;
16use std::collections::{BTreeMap, BTreeSet};
17use std::io::{Cursor, Write};
18
19#[derive(Debug, Clone, Default)]
21pub struct RustBackend;
22
23impl CodeGenBackend for RustBackend {
24 fn generate(
25 &self,
26 schemas: &[JsonSchema],
27 settings: &CodeGenSettings,
28 ) -> CodeGenResult<GenerateRustOutput> {
29 match settings.dedupe_mode {
30 DedupeMode::Disabled => {
31 let mut per_schema: Vec<Vec<u8>> = Vec::with_capacity(schemas.len());
32 for (index, schema) in schemas.iter().enumerate() {
33 let mut out = Cursor::new(Vec::new());
34 emit_rust(schema, &mut out, settings).map_err(|e| CodeGenError::Batch {
35 index,
36 source: Box::new(e),
37 })?;
38 per_schema.push({
39 let result = maybe_prepend_btreemap_use(out.into_inner());
40 let result = maybe_prepend_hash_set_use(result);
41 #[cfg(feature = "uuid")]
42 let result = maybe_prepend_uuid_use(result);
43 result
44 });
45 }
46 Ok(GenerateRustOutput {
47 shared: None,
48 per_schema,
49 })
50 }
51 DedupeMode::Functional | DedupeMode::Full => {
52 generate_rust_with_dedupe(schemas, settings)
53 }
54 }
55 }
56}
57
58struct StructToEmit {
60 name: String,
61 schema: JsonSchema,
62}
63
64type EnumValuesToNameMap =
66 BTreeMap<Vec<String>, (String, Option<String>, Option<Vec<serde_json::Value>>)>;
67
68struct EnumToEmit {
70 name: String,
71 values: Vec<String>,
72 description: Option<String>,
73 examples: Option<Vec<serde_json::Value>>,
74}
75
76struct AnyOfEnumToEmit {
78 name: String,
79 variants: Vec<(String, String)>,
80}
81
82struct OneOfEnumToEmit {
84 name: String,
85 variants: Vec<(String, String)>,
86}
87
88fn doc_lines(s: Option<&str>) -> Vec<String> {
90 let Some(trimmed) = s.map(str::trim) else {
91 return Vec::new();
92 };
93 if trimmed.is_empty() {
94 return Vec::new();
95 }
96 trimmed
97 .split('\n')
98 .map(str::trim)
99 .filter(|line| !line.is_empty())
100 .map(String::from)
101 .collect()
102}
103
104fn examples_doc_lines(examples: Option<&[serde_json::Value]>) -> Vec<String> {
107 let Some(examples) = examples else {
108 return Vec::new();
109 };
110 if examples.is_empty() {
111 return Vec::new();
112 }
113 let mut lines = vec![String::new(), "# Examples".to_string(), String::new()];
114 for ex in examples {
115 if let Ok(s) = serde_json::to_string(ex) {
116 lines.push(s);
117 }
118 }
119 lines
120}
121
122fn emit_deprecated_attr(out: &mut impl Write, schema: &JsonSchema) -> CodeGenResult<()> {
125 if schema.deprecated != Some(true) {
126 return Ok(());
127 }
128 let msg: Option<String> = schema
129 .description
130 .as_deref()
131 .map(str::trim)
132 .filter(|s| !s.is_empty())
133 .and_then(|s| s.split('\n').next())
134 .map(str::trim)
135 .filter(|s| !s.is_empty())
136 .map(|s| s.replace('\\', "\\\\").replace('"', "\\\""));
137 if let Some(m) = msg {
138 writeln!(out, " #[deprecated = \"{m}\"]")?;
139 } else {
140 writeln!(out, " #[deprecated]")?;
141 }
142 Ok(())
143}
144
145fn emit_struct_deprecated_attr(out: &mut impl Write, schema: &JsonSchema) -> CodeGenResult<()> {
147 if schema.deprecated != Some(true) {
148 return Ok(());
149 }
150 let msg: Option<String> = schema
151 .description
152 .as_deref()
153 .map(str::trim)
154 .filter(|s| !s.is_empty())
155 .and_then(|s| s.split('\n').next())
156 .map(str::trim)
157 .filter(|s| !s.is_empty())
158 .map(|s| s.replace('\\', "\\\\").replace('"', "\\\""));
159 if let Some(m) = msg {
160 writeln!(out, "#[deprecated = \"{m}\"]")?;
161 } else {
162 writeln!(out, "#[deprecated]")?;
163 }
164 Ok(())
165}
166
167fn string_enum_values(schema: &JsonSchema) -> Option<Vec<String>> {
169 if !schema.is_string_enum() {
170 return None;
171 }
172 let v: Vec<String> = schema
173 .enum_values
174 .as_ref()
175 .expect("string enum")
176 .iter()
177 .filter_map(|x| x.as_str().map(String::from))
178 .collect();
179 let set: BTreeSet<String> = v.into_iter().collect();
180 let mut out: Vec<String> = set.iter().cloned().collect();
181 out.sort();
182 Some(out)
183}
184
185fn string_enum_or_const_values(schema: &JsonSchema) -> Option<Vec<String>> {
187 if let Some(values) = string_enum_values(schema) {
188 return Some(values);
189 }
190 if schema.is_string_const() {
191 let s: String = schema
192 .const_value
193 .as_ref()
194 .and_then(|v| v.as_str().map(String::from))
195 .expect("string const");
196 return Some(vec![s]);
197 }
198 None
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
203enum AdditionalPropertiesDedupe {
204 Forbid,
205 Schema(Box<DedupeKey>),
206}
207
208#[derive(Debug, Clone)]
211struct DedupeKey {
212 id: Option<String>,
213 type_: Option<String>,
214 properties: BTreeMap<String, DedupeKey>,
215 additional_properties: Option<AdditionalPropertiesDedupe>,
216 required: Option<Vec<String>>,
217 title: Option<String>,
218 description: Option<String>,
219 comment: Option<String>,
220 deprecated: Option<bool>,
221 examples: Option<Vec<serde_json::Value>>,
222 items: Option<Box<DedupeKey>>,
223 unique_items: Option<bool>,
224 min_items: Option<u64>,
225 max_items: Option<u64>,
226 min_length: Option<u64>,
227 max_length: Option<u64>,
228 pattern: Option<String>,
229 format: Option<String>,
230 default_value: Option<serde_json::Value>,
231}
232
233impl PartialEq for DedupeKey {
234 fn eq(&self, other: &Self) -> bool {
235 self.id == other.id
236 && self.type_ == other.type_
237 && self.properties == other.properties
238 && self.additional_properties == other.additional_properties
239 && self.required == other.required
240 && self.title == other.title
241 && self.description == other.description
242 && self.comment == other.comment
243 && self.deprecated == other.deprecated
244 && self.examples == other.examples
245 && self.items == other.items
246 && self.unique_items == other.unique_items
247 && self.min_items == other.min_items
248 && self.max_items == other.max_items
249 && self.min_length == other.min_length
250 && self.max_length == other.max_length
251 && self.pattern == other.pattern
252 && self.format == other.format
253 && self.default_value == other.default_value
254 }
255}
256
257impl Eq for DedupeKey {}
258
259impl PartialOrd for DedupeKey {
260 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
261 Some(self.cmp(other))
262 }
263}
264
265impl Ord for DedupeKey {
266 fn cmp(&self, other: &Self) -> Ordering {
267 self.id
268 .cmp(&other.id)
269 .then_with(|| self.type_.cmp(&other.type_))
270 .then_with(|| {
271 self.properties
272 .keys()
273 .cmp(other.properties.keys())
274 .then_with(|| {
275 for (k, v) in &self.properties {
276 if let Some(ov) = other.properties.get(k) {
277 let c = v.cmp(ov);
278 if c != Ordering::Equal {
279 return c;
280 }
281 }
282 }
283 self.properties.len().cmp(&other.properties.len())
284 })
285 })
286 .then_with(|| self.additional_properties.cmp(&other.additional_properties))
287 .then_with(|| compare_option_vec(self.required.as_ref(), other.required.as_ref()))
288 .then_with(|| self.title.cmp(&other.title))
289 .then_with(|| self.description.cmp(&other.description))
290 .then_with(|| self.comment.cmp(&other.comment))
291 .then_with(|| self.deprecated.cmp(&other.deprecated))
292 .then_with(|| compare_option_vec_value(self.examples.as_ref(), other.examples.as_ref()))
293 .then_with(|| self.items.cmp(&other.items))
294 .then_with(|| self.unique_items.cmp(&other.unique_items))
295 .then_with(|| self.min_items.cmp(&other.min_items))
296 .then_with(|| self.max_items.cmp(&other.max_items))
297 .then_with(|| self.min_length.cmp(&other.min_length))
298 .then_with(|| self.max_length.cmp(&other.max_length))
299 .then_with(|| self.pattern.cmp(&other.pattern))
300 .then_with(|| self.format.cmp(&other.format))
301 .then_with(|| {
302 compare_option_value(self.default_value.as_ref(), other.default_value.as_ref())
303 })
304 }
305}
306
307fn compare_option_value(a: Option<&serde_json::Value>, b: Option<&serde_json::Value>) -> Ordering {
308 match (a, b) {
309 (None, None) => Ordering::Equal,
310 (None, Some(_)) => Ordering::Less,
311 (Some(_), None) => Ordering::Greater,
312 (Some(a_val), Some(b_val)) => {
313 let a_str: String = serde_json::to_string(a_val).unwrap_or_default();
314 let b_str: String = serde_json::to_string(b_val).unwrap_or_default();
315 a_str.cmp(&b_str)
316 }
317 }
318}
319
320fn compare_option_vec_value(
321 a: Option<&Vec<serde_json::Value>>,
322 b: Option<&Vec<serde_json::Value>>,
323) -> Ordering {
324 match (a, b) {
325 (None, None) => Ordering::Equal,
326 (None, Some(_)) => Ordering::Less,
327 (Some(_), None) => Ordering::Greater,
328 (Some(aa), Some(bb)) => {
329 let len_cmp: Ordering = aa.len().cmp(&bb.len());
330 if len_cmp != Ordering::Equal {
331 return len_cmp;
332 }
333 for (a_val, b_val) in aa.iter().zip(bb.iter()) {
334 let c: Ordering = compare_option_value(Some(a_val), Some(b_val));
335 if c != Ordering::Equal {
336 return c;
337 }
338 }
339 Ordering::Equal
340 }
341 }
342}
343
344fn json_value_equals_rust_type_default(
346 value: &serde_json::Value,
347 _type_str: &str,
348 is_optional: bool,
349) -> bool {
350 if is_optional {
351 return value.is_null();
352 }
353 match value {
354 serde_json::Value::Null => true,
355 serde_json::Value::Bool(b) => !*b,
356 serde_json::Value::Number(n) => {
357 if n.as_i64() == Some(0) {
358 return true;
359 }
360 if n.as_f64() == Some(0.0) {
361 return true;
362 }
363 false
364 }
365 serde_json::Value::String(s) => s.is_empty(),
366 serde_json::Value::Array(a) => a.is_empty(),
367 serde_json::Value::Object(o) => o.is_empty(),
368 }
369}
370
371fn default_function_name(struct_name: &str, field_name: &str) -> String {
374 let struct_snake: String = struct_name.to_snake_case();
375 let field_snake: String = field_name.to_snake_case();
376 format!("default_{struct_snake}_{field_snake}")
377}
378
379fn json_default_to_rust_expr(
381 value: &serde_json::Value,
382 _ty: &str,
383 is_optional: bool,
384) -> Option<String> {
385 let inner: String = match value {
386 serde_json::Value::Bool(b) => b.to_string(),
387 serde_json::Value::Number(n) => {
388 if n.is_i64() {
389 n.as_i64().unwrap().to_string()
390 } else {
391 n.as_f64().unwrap().to_string()
392 }
393 }
394 serde_json::Value::String(s) => {
395 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
396 format!("\"{escaped}\".to_string()")
397 }
398 serde_json::Value::Null | serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
399 return None;
400 }
401 };
402 let expr: String = if is_optional {
403 format!("Some({inner})")
404 } else {
405 inner
406 };
407 Some(expr)
408}
409
410fn compare_option_vec(a: Option<&Vec<String>>, b: Option<&Vec<String>>) -> Ordering {
411 match (a, b) {
412 (None, None) => Ordering::Equal,
413 (None, Some(_)) => Ordering::Less,
414 (Some(_), None) => Ordering::Greater,
415 (Some(aa), Some(bb)) => aa.cmp(bb),
416 }
417}
418
419fn maybe_prepend_btreemap_use(mut buf: Vec<u8>) -> Vec<u8> {
421 if !buf.windows(8).any(|w| w == b"BTreeMap") {
422 return buf;
423 }
424 let needle = b"use serde::{Deserialize, Serialize};\n";
425 let pos = buf
426 .windows(needle.len())
427 .position(|w| w == needle)
428 .map(|i| i + needle.len());
429 if let Some(insert_at) = pos {
430 let line = b"use std::collections::BTreeMap;\n";
431 buf.splice(insert_at..insert_at, line.iter().copied());
432 }
433 buf
434}
435
436fn maybe_prepend_hash_set_use(mut buf: Vec<u8>) -> Vec<u8> {
437 if !buf.windows(7).any(|w| w == b"HashSet") {
438 return buf;
439 }
440 let needle = b"use serde::{Deserialize, Serialize};\n";
441 let pos = buf
442 .windows(needle.len())
443 .position(|w| w == needle)
444 .map(|i| i + needle.len());
445 if let Some(insert_at) = pos {
446 let hash_set_use = b"use std::collections::HashSet;\n";
447 buf.splice(insert_at..insert_at, hash_set_use.iter().copied());
448 }
449 buf
450}
451
452#[cfg(feature = "uuid")]
454fn maybe_prepend_uuid_use(mut buf: Vec<u8>) -> Vec<u8> {
455 if !buf.windows(4).any(|w| w == b"Uuid") {
456 return buf;
457 }
458 let hash_set_needle = b"use std::collections::HashSet;\n";
459 let serde_needle = b"use serde::{Deserialize, Serialize};\n";
460 let insert_at = buf
461 .windows(hash_set_needle.len())
462 .position(|w| w == hash_set_needle)
463 .map(|i| i + hash_set_needle.len())
464 .or_else(|| {
465 buf.windows(serde_needle.len())
466 .position(|w| w == serde_needle)
467 .map(|i| i + serde_needle.len())
468 });
469 if let Some(pos) = insert_at {
470 let uuid_use = b"use uuid::Uuid;\n";
471 buf.splice(pos..pos, uuid_use.iter().copied());
472 }
473 buf
474}
475
476fn emit_struct_derive_and_attrs(
480 out: &mut impl Write,
481 name: &str,
482 schema: &JsonSchema,
483) -> CodeGenResult<()> {
484 for line in doc_lines(schema.description.as_deref()) {
485 writeln!(out, "/// {line}")?;
486 }
487 for line in examples_doc_lines(schema.examples.as_deref()) {
488 writeln!(out, "/// {line}")?;
489 }
490 emit_struct_deprecated_attr(out, schema)?;
491 writeln!(
492 out,
493 "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]"
494 )?;
495 if let Some(ref t) = schema.title {
496 let t = t.trim();
497 if !t.is_empty() {
498 let escaped = t.replace('\\', "\\\\").replace('"', "\\\"");
499 writeln!(out, "#[json_schema(title = \"{escaped}\")]")?;
500 }
501 }
502 if let Some(ref i) = schema.id {
503 let escaped = i.replace('\\', "\\\\").replace('"', "\\\"");
504 writeln!(out, "#[json_schema(id = \"{escaped}\")]")?;
505 }
506 if schema
507 .additional_properties
508 .as_ref()
509 .is_some_and(|ap| matches!(ap, AdditionalProperties::Forbid))
510 {
511 writeln!(out, "#[serde(deny_unknown_fields)]")?;
512 }
513 writeln!(out, "pub struct {name} {{")?;
514 Ok(())
515}
516
517impl DedupeKey {
518 fn from_schema(schema: &JsonSchema, mode: DedupeMode) -> Self {
519 let properties: BTreeMap<String, DedupeKey> = schema
520 .properties
521 .iter()
522 .map(|(k, v)| (k.clone(), DedupeKey::from_schema(v, mode)))
523 .collect();
524 let items: Option<Box<DedupeKey>> = schema
525 .items
526 .as_ref()
527 .filter(|_| schema.type_.as_deref() == Some("array"))
528 .map(|s| Box::new(DedupeKey::from_schema(s, mode)));
529 let unique_items: Option<bool> = if schema.type_.as_deref() == Some("array") {
530 schema.unique_items
531 } else {
532 None
533 };
534 let min_items: Option<u64> = if schema.type_.as_deref() == Some("array") {
535 schema.min_items
536 } else {
537 None
538 };
539 let max_items: Option<u64> = if schema.type_.as_deref() == Some("array") {
540 schema.max_items
541 } else {
542 None
543 };
544 let min_length: Option<u64> = if schema.type_.as_deref() == Some("string") {
545 schema.min_length
546 } else {
547 None
548 };
549 let max_length: Option<u64> = if schema.type_.as_deref() == Some("string") {
550 schema.max_length
551 } else {
552 None
553 };
554 let pattern: Option<String> = if schema.type_.as_deref() == Some("string") {
555 schema.pattern.clone()
556 } else {
557 None
558 };
559 let format: Option<String> = if schema.type_.as_deref() == Some("string") {
560 schema.format.clone()
561 } else {
562 None
563 };
564 let additional_properties: Option<AdditionalPropertiesDedupe> =
565 match schema.additional_properties.as_ref() {
566 None | Some(AdditionalProperties::Allow) => None,
567 Some(AdditionalProperties::Forbid) => Some(AdditionalPropertiesDedupe::Forbid),
568 Some(AdditionalProperties::Schema(s)) => Some(AdditionalPropertiesDedupe::Schema(
569 Box::new(DedupeKey::from_schema(s, mode)),
570 )),
571 };
572 DedupeKey {
573 id: match mode {
574 DedupeMode::Full => schema.id.clone(),
575 DedupeMode::Functional | DedupeMode::Disabled => None,
576 },
577 type_: schema.type_.clone(),
578 properties,
579 additional_properties,
580 required: schema.required.clone(),
581 title: schema.title.clone(),
582 description: match mode {
583 DedupeMode::Full => schema.description.clone(),
584 DedupeMode::Functional | DedupeMode::Disabled => None,
585 },
586 comment: match mode {
587 DedupeMode::Full => schema.comment.clone(),
588 DedupeMode::Functional | DedupeMode::Disabled => None,
589 },
590 deprecated: match mode {
591 DedupeMode::Full => schema.deprecated,
592 DedupeMode::Functional | DedupeMode::Disabled => None,
593 },
594 examples: match mode {
595 DedupeMode::Full => schema.examples.clone(),
596 DedupeMode::Functional | DedupeMode::Disabled => None,
597 },
598 items,
599 unique_items,
600 min_items,
601 max_items,
602 min_length,
603 max_length,
604 pattern,
605 format,
606 default_value: schema.default_value.clone(),
607 }
608 }
609}
610
611fn struct_name_from(
613 title: Option<&str>,
614 from_key: Option<&str>,
615 is_root: bool,
616 settings: &CodeGenSettings,
617) -> String {
618 let title_trimmed: Option<&str> = title.filter(|t| !t.trim().is_empty()).map(str::trim);
619 let from_key_s: Option<&str> = from_key;
620
621 let (first, second) = match settings.model_name_source {
622 ModelNameSource::TitleFirst => (title_trimmed, from_key_s),
623 ModelNameSource::PropertyKeyFirst => (from_key_s, title_trimmed),
624 };
625
626 first
627 .map(sanitize_struct_name)
628 .or_else(|| second.map(sanitize_struct_name))
629 .unwrap_or_else(|| {
630 if is_root {
631 "Root".to_string()
632 } else {
633 "Unnamed".to_string()
634 }
635 })
636}
637
638const I64_MAX_AS_F64: f64 = 9_223_372_036_854_775_807.0_f64; fn rust_numeric_type_for_schema(schema: &JsonSchema) -> String {
642 if schema.is_integer() {
643 let min: Option<f64> = schema.minimum;
644 let max: Option<f64> = schema.maximum;
645 #[expect(clippy::cast_precision_loss)]
646 let i64_min_f64: f64 = i64::MIN as f64;
647 let (min_i64, max_i64): (Option<i64>, Option<i64>) = match (min, max) {
648 (Some(mi), Some(ma)) if mi <= ma => {
649 let valid_min: bool =
650 mi.fract() == 0.0 && (i64_min_f64..=I64_MAX_AS_F64).contains(&mi);
651 let valid_max: bool =
652 ma.fract() == 0.0 && (i64_min_f64..=I64_MAX_AS_F64).contains(&ma);
653 if valid_min && valid_max {
654 #[expect(clippy::cast_possible_truncation)]
655 let min_i: i64 = mi as i64;
656 #[expect(clippy::cast_possible_truncation)]
657 let max_i: i64 = ma as i64;
658 (Some(min_i), Some(max_i))
659 } else {
660 (None, None)
661 }
662 }
663 _ => (None, None),
664 };
665 if let (Some(lo), Some(hi)) = (min_i64, max_i64) {
666 if lo >= 0 {
667 if hi <= i64::from(u8::MAX) {
668 return "u8".to_string();
669 }
670 if hi <= i64::from(u16::MAX) {
671 return "u16".to_string();
672 }
673 if hi <= i64::from(u32::MAX) {
674 return "u32".to_string();
675 }
676 return "u64".to_string();
677 }
678 if lo >= i64::from(i8::MIN) && hi <= i64::from(i8::MAX) {
679 return "i8".to_string();
680 }
681 if lo >= i64::from(i16::MIN) && hi <= i64::from(i16::MAX) {
682 return "i16".to_string();
683 }
684 if lo >= i64::from(i32::MIN) && hi <= i64::from(i32::MAX) {
685 return "i32".to_string();
686 }
687 }
688 return "i64".to_string();
689 }
690 if schema.is_number() {
691 let min: Option<f64> = schema.minimum;
692 let max: Option<f64> = schema.maximum;
693 if let (Some(mi), Some(ma)) = (min, max)
694 && mi <= ma
695 && mi >= f64::from(f32::MIN)
696 && ma <= f64::from(f32::MAX)
697 && mi.is_finite()
698 && ma.is_finite()
699 {
700 return "f32".to_string();
701 }
702 return "f64".to_string();
703 }
704 unreachable!("rust_numeric_type_for_schema only called for integer or number schema");
705}
706
707fn item_schema_is_hashable(schema: &JsonSchema) -> bool {
710 !schema.is_object_with_properties() && !schema.is_array_with_items()
711}
712
713fn is_object_like_for_merge(schema: &JsonSchema) -> bool {
715 schema.type_.as_deref() == Some("object") || !schema.properties.is_empty()
716}
717
718pub(crate) fn merge_all_of(schemas: &[JsonSchema]) -> CodeGenResult<JsonSchema> {
721 if schemas.is_empty() {
722 return Err(CodeGenError::AllOfMergeEmpty);
723 }
724 for (index, s) in schemas.iter().enumerate() {
725 if !is_object_like_for_merge(s) {
726 return Err(CodeGenError::AllOfMergeNonObjectSubschema { index });
727 }
728 }
729 let mut merged = JsonSchema::default();
730 for s in schemas {
731 merge_object_schema_into(&mut merged, s, "")?;
732 }
733 merged.type_ = Some("object".to_string());
734 Ok(merged)
735}
736
737fn merge_object_schema_into(
739 target: &mut JsonSchema,
740 other: &JsonSchema,
741 parent_key: &str,
742) -> CodeGenResult<()> {
743 for (k, other_prop) in &other.properties {
744 let key_for_errors = if parent_key.is_empty() {
745 k.clone()
746 } else {
747 format!("{parent_key}.{k}")
748 };
749 if let Some(target_prop) = target.properties.get_mut(k) {
750 let merged_prop = merge_property_schemas(target_prop, other_prop, &key_for_errors)?;
751 *target_prop = merged_prop;
752 } else {
753 target.properties.insert(k.clone(), other_prop.clone());
754 }
755 }
756 let mut required: Vec<String> = target.required.clone().unwrap_or_default();
758 for r in other.required.as_deref().unwrap_or(&[]) {
759 if !required.contains(r) {
760 required.push(r.clone());
761 }
762 }
763 target.required = if required.is_empty() {
764 None
765 } else {
766 Some(required)
767 };
768 if target.title.as_deref().map_or("", str::trim).is_empty() {
769 target.title.clone_from(&other.title);
770 }
771 if target
772 .description
773 .as_deref()
774 .map_or("", str::trim)
775 .is_empty()
776 {
777 target.description.clone_from(&other.description);
778 }
779 if target.comment.is_none() {
780 target.comment.clone_from(&other.comment);
781 }
782 if target.deprecated.is_none() {
783 target.deprecated = other.deprecated;
784 }
785 if target.examples.is_none() {
786 target.examples.clone_from(&other.examples);
787 }
788 Ok(())
789}
790
791fn merge_property_schemas(
793 a: &JsonSchema,
794 b: &JsonSchema,
795 property_key: &str,
796) -> CodeGenResult<JsonSchema> {
797 if a.is_object_with_properties() && b.is_object_with_properties() {
798 let mut merged = a.clone();
799 merge_object_schema_into(&mut merged, b, property_key)?;
800 return Ok(merged);
801 }
802 if a.is_array_with_items() && b.is_array_with_items() {
803 let a_items = a.items.as_ref().expect("array with items").as_ref();
804 let b_items = b.items.as_ref().expect("array with items").as_ref();
805 let merged_items = merge_property_schemas(a_items, b_items, &format!("{property_key}[]"))?;
806 let mut out = a.clone();
807 out.items = Some(Box::new(merged_items));
808 return Ok(out);
809 }
810 let type_a = a.type_.as_deref();
811 let type_b = b.type_.as_deref();
812 if type_a != type_b {
813 return Err(CodeGenError::AllOfMergeConflictingPropertyType {
814 property_key: property_key.to_string(),
815 subschema_indices: vec![], });
817 }
818 if a.is_string() && b.is_string() {
819 let mut out = a.clone();
820 if out.min_length.is_none() {
821 out.min_length = b.min_length;
822 }
823 if out.max_length.is_none() {
824 out.max_length = b.max_length;
825 }
826 if let (Some(pa), Some(pb)) = (&a.pattern, &b.pattern) {
827 if pa != pb {
828 return Err(CodeGenError::AllOfMergeConflictingPattern {
829 property_key: property_key.to_string(),
830 });
831 }
832 } else if out.pattern.is_none() {
833 out.pattern.clone_from(&b.pattern);
834 }
835 if out.format.is_none() {
836 out.format.clone_from(&b.format);
837 }
838 if let (Some(ea), Some(eb)) = (&a.enum_values, &b.enum_values) {
839 if ea != eb {
840 return Err(CodeGenError::AllOfMergeConflictingEnum {
841 property_key: property_key.to_string(),
842 });
843 }
844 } else if b.enum_values.is_some() {
845 out.enum_values.clone_from(&b.enum_values);
846 }
847 if let (Some(ca), Some(cb)) = (&a.const_value, &b.const_value) {
848 if ca != cb {
849 return Err(CodeGenError::AllOfMergeConflictingConst {
850 property_key: property_key.to_string(),
851 });
852 }
853 } else if b.const_value.is_some() {
854 out.const_value.clone_from(&b.const_value);
855 }
856 return Ok(out);
857 }
858 if a.is_integer() && b.is_integer() || a.is_number() && b.is_number() {
859 let mut out = a.clone();
860 merge_numeric_bounds(&mut out, b, property_key, "minimum", "maximum")?;
861 return Ok(out);
862 }
863 if a.is_string_enum() && b.is_string_enum() {
864 if a.enum_values != b.enum_values {
865 return Err(CodeGenError::AllOfMergeConflictingEnum {
866 property_key: property_key.to_string(),
867 });
868 }
869 return Ok(a.clone());
870 }
871 if type_a.is_some() || type_b.is_some() {
872 return Err(CodeGenError::AllOfMergeConflictingPropertyType {
873 property_key: property_key.to_string(),
874 subschema_indices: vec![],
875 });
876 }
877 Ok(a.clone())
878}
879
880fn merge_numeric_bounds(
881 target: &mut JsonSchema,
882 other: &JsonSchema,
883 property_key: &str,
884 min_kw: &str,
885 max_kw: &str,
886) -> CodeGenResult<()> {
887 let (t_min, t_max) = (target.minimum, target.maximum);
888 let (o_min, o_max) = (other.minimum, other.maximum);
889 let new_min = match (t_min, o_min) {
890 (Some(t), Some(o)) => Some(t.max(o)),
891 (a, None) | (None, a) => a,
892 };
893 let new_max = match (t_max, o_max) {
894 (Some(t), Some(o)) => Some(t.min(o)),
895 (a, None) | (None, a) => a,
896 };
897 if let (Some(mi), Some(ma)) = (new_min, new_max)
898 && mi > ma
899 {
900 return Err(CodeGenError::AllOfMergeConflictingNumericBounds {
901 property_key: property_key.to_string(),
902 keyword: format!("{min_kw}/{max_kw}"),
903 });
904 }
905 target.minimum = new_min;
906 target.maximum = new_max;
907 Ok(())
908}
909
910pub(crate) fn resolve_all_of_for_codegen(schema: &JsonSchema) -> CodeGenResult<JsonSchema> {
912 match &schema.all_of {
913 Some(all) if !all.is_empty() => merge_all_of(all),
914 Some(_) => Err(CodeGenError::AllOfMergeEmpty),
915 None => Ok(schema.clone()),
916 }
917}
918
919fn rust_type_for_item_schema(
922 root: &JsonSchema,
923 schema: &JsonSchema,
924 from_key: Option<&str>,
925 enum_values_to_name: Option<&BTreeMap<Vec<String>, String>>,
926 key_to_name: Option<&BTreeMap<DedupeKey, String>>,
927 settings: &CodeGenSettings,
928 mode: DedupeMode,
929) -> CodeGenResult<String> {
930 let mut def_key: Option<String> = None;
931 let schema: &JsonSchema = if let Some(ref_str) = schema.ref_.as_deref() {
932 match ref_resolver::parse_ref(ref_str) {
933 Ok(
934 ref_resolver::ParsedRef::Defs(name) | ref_resolver::ParsedRef::Definitions(name),
935 ) => def_key = Some(name),
936 Ok(ref_resolver::ParsedRef::Root) => {}
937 Err(e) => {
938 return Err(CodeGenError::RefResolution {
939 ref_str: ref_str.to_string(),
940 reason: format!("{e:?}"),
941 });
942 }
943 }
944
945 ref_resolver::resolve_schema_ref_transitive(root, schema).map_err(|e| {
946 CodeGenError::RefResolution {
947 ref_str: ref_str.to_string(),
948 reason: format!("{e:?}"),
949 }
950 })?
951 } else {
952 schema
953 };
954 if let Some(values) = string_enum_or_const_values(schema)
955 && let Some(m) = enum_values_to_name
956 && let Some(name) = m.get(&values)
957 {
958 return Ok(name.clone());
959 }
960 if schema.is_string()
961 || (schema.enum_values.as_ref().is_some_and(|v| !v.is_empty()) && !schema.is_string_enum())
962 || (schema.const_value.is_some() && !schema.is_string_const())
963 {
964 #[cfg(feature = "uuid")]
965 {
966 if schema.format.as_deref() == Some("uuid") {
967 return Ok("Uuid".to_string());
968 }
969 }
970 return Ok("String".to_string());
971 }
972 if schema.is_integer() {
973 return Ok(rust_numeric_type_for_schema(schema));
974 }
975 if schema.is_number() {
976 return Ok(rust_numeric_type_for_schema(schema));
977 }
978 if schema.is_boolean() {
979 return Ok("bool".to_string());
980 }
981 if schema.is_object_with_properties() {
982 if let Some(key) = def_key.as_deref() {
983 return Ok(sanitize_struct_name(key));
984 }
985 let name: String = if let Some(m) = key_to_name {
986 let key = DedupeKey::from_schema(schema, mode);
987 m.get(&key).cloned().unwrap_or_else(|| {
988 struct_name_from(schema.title.as_deref(), from_key, false, settings)
989 })
990 } else {
991 struct_name_from(schema.title.as_deref(), from_key, false, settings)
992 };
993 return Ok(name);
994 }
995 if schema.is_array_with_items() {
996 let item_schema: &JsonSchema = schema.items.as_ref().expect("array with items").as_ref();
997 let inner: String = rust_type_for_item_schema(
998 root,
999 item_schema,
1000 from_key,
1001 enum_values_to_name,
1002 key_to_name,
1003 settings,
1004 mode,
1005 )?;
1006 let use_hash_set: bool =
1007 schema.unique_items == Some(true) && item_schema_is_hashable(item_schema);
1008 return Ok(if use_hash_set {
1009 format!("HashSet<{inner}>")
1010 } else {
1011 format!("Vec<{inner}>")
1012 });
1013 }
1014 Ok("serde_json::Value".to_string())
1015}
1016
1017fn collect_enums(
1019 root: &JsonSchema,
1020 schema: &JsonSchema,
1021 settings: &CodeGenSettings,
1022) -> CodeGenResult<Vec<EnumToEmit>> {
1023 let mut key_to_name_desc_examples: EnumValuesToNameMap = BTreeMap::new();
1024 let mut stack: Vec<JsonSchema> = vec![schema.clone()];
1025 while let Some(node) = stack.pop() {
1026 let (node, _) = resolve_ref_for_codegen(root, &node, None)?;
1027 for (key, prop_schema) in &node.properties {
1028 let (prop_effective, from_key) = resolve_ref_for_codegen(root, prop_schema, Some(key))?;
1029 if let Some(values) = string_enum_or_const_values(&prop_effective) {
1030 key_to_name_desc_examples
1031 .entry(values.clone())
1032 .or_insert_with(|| {
1033 let name: String = struct_name_from(
1034 prop_effective.title.as_deref(),
1035 from_key.as_deref(),
1036 false,
1037 settings,
1038 );
1039 let description: Option<String> = prop_effective
1040 .description
1041 .as_ref()
1042 .filter(|s| !s.trim().is_empty())
1043 .cloned();
1044 let examples: Option<Vec<serde_json::Value>> =
1045 prop_effective.examples.clone();
1046 (name, description, examples)
1047 });
1048 }
1049 if prop_effective.is_object_with_properties() {
1050 stack.push(prop_effective.clone());
1051 }
1052 if prop_effective.is_array_with_items()
1053 && let Some(ref items) = prop_effective.items
1054 {
1055 let (items_effective, items_from_key) =
1056 resolve_ref_for_codegen(root, items.as_ref(), Some(key))?;
1057 if let Some(values) = string_enum_or_const_values(&items_effective) {
1058 key_to_name_desc_examples
1059 .entry(values.clone())
1060 .or_insert_with(|| {
1061 let name: String = struct_name_from(
1062 items_effective.title.as_deref(),
1063 items_from_key.as_deref(),
1064 false,
1065 settings,
1066 );
1067 let description: Option<String> = items_effective
1068 .description
1069 .as_ref()
1070 .filter(|s| !s.trim().is_empty())
1071 .cloned();
1072 let examples: Option<Vec<serde_json::Value>> =
1073 items_effective.examples.clone();
1074 (name, description, examples)
1075 });
1076 }
1077 if items_effective.is_object_with_properties() {
1078 stack.push(items_effective);
1079 }
1080 }
1081 }
1082 if let Some(ref any_of) = node.any_of {
1083 for sub in any_of {
1084 stack.push(sub.clone());
1085 }
1086 }
1087 if let Some(ref one_of) = node.one_of {
1088 for sub in one_of {
1089 stack.push(sub.clone());
1090 }
1091 }
1092 }
1093 Ok(key_to_name_desc_examples
1094 .into_iter()
1095 .map(|(values, (name, description, examples))| EnumToEmit {
1096 name,
1097 values,
1098 description,
1099 examples,
1100 })
1101 .collect())
1102}
1103
1104fn collect_anyof_enums(
1106 root: &JsonSchema,
1107 schema: &JsonSchema,
1108 settings: &CodeGenSettings,
1109 enum_values_to_name: &BTreeMap<Vec<String>, String>,
1110) -> CodeGenResult<Vec<AnyOfEnumToEmit>> {
1111 let mut out: Vec<AnyOfEnumToEmit> = vec![];
1112 let mut stack: Vec<(JsonSchema, Option<String>)> = vec![(schema.clone(), None)];
1113 while let Some((node, from_key)) = stack.pop() {
1114 let (node, from_key) = resolve_ref_for_codegen(root, &node, from_key.as_deref())?;
1115 if let Some(ref any_of) = node.any_of {
1116 if any_of.is_empty() {
1117 return Err(CodeGenError::AnyOfEmpty);
1118 }
1119 let name = match &from_key {
1120 Some(k) => sanitize_struct_name(k) + "AnyOf",
1121 None => node.title.as_deref().map_or_else(
1122 || "RootAnyOf".to_string(),
1123 |t| sanitize_struct_name(t) + "AnyOf",
1124 ),
1125 };
1126 let mut variants = Vec::with_capacity(any_of.len());
1127 for (i, sub) in any_of.iter().enumerate() {
1128 let resolved = resolve_all_of_for_codegen(sub)?;
1129 let variant_from_key =
1130 format!("{}_Variant{i}", from_key.as_deref().unwrap_or("Root"));
1131 let ty = rust_type_for_item_schema(
1132 root,
1133 &resolved,
1134 Some(&variant_from_key),
1135 Some(enum_values_to_name),
1136 None,
1137 settings,
1138 DedupeMode::Full,
1139 )?;
1140 variants.push((format!("Variant{i}"), ty));
1141 }
1142 out.push(AnyOfEnumToEmit { name, variants });
1143 for sub in any_of {
1144 let resolved = resolve_all_of_for_codegen(sub)?;
1145 stack.push((resolved, None));
1146 }
1147 }
1148 for (key, prop_schema) in &node.properties {
1149 stack.push((prop_schema.clone(), Some(key.clone())));
1150 }
1151 }
1152 Ok(out)
1153}
1154
1155fn collect_oneof_enums(
1157 root: &JsonSchema,
1158 schema: &JsonSchema,
1159 settings: &CodeGenSettings,
1160 enum_values_to_name: &BTreeMap<Vec<String>, String>,
1161) -> CodeGenResult<Vec<OneOfEnumToEmit>> {
1162 let mut out: Vec<OneOfEnumToEmit> = vec![];
1163 let mut stack: Vec<(JsonSchema, Option<String>)> = vec![(schema.clone(), None)];
1164 while let Some((node, from_key)) = stack.pop() {
1165 let (node, from_key) = resolve_ref_for_codegen(root, &node, from_key.as_deref())?;
1166 if let Some(ref one_of) = node.one_of {
1167 if one_of.is_empty() {
1168 return Err(CodeGenError::OneOfEmpty);
1169 }
1170 let name = match &from_key {
1171 Some(k) => sanitize_struct_name(k) + "OneOf",
1172 None => node.title.as_deref().map_or_else(
1173 || "RootOneOf".to_string(),
1174 |t| sanitize_struct_name(t) + "OneOf",
1175 ),
1176 };
1177 let mut variants = Vec::with_capacity(one_of.len());
1178 for (i, sub) in one_of.iter().enumerate() {
1179 let resolved = resolve_all_of_for_codegen(sub)?;
1180 let variant_from_key =
1181 format!("{}_Variant{i}", from_key.as_deref().unwrap_or("Root"));
1182 let ty = rust_type_for_item_schema(
1183 root,
1184 &resolved,
1185 Some(&variant_from_key),
1186 Some(enum_values_to_name),
1187 None,
1188 settings,
1189 DedupeMode::Full,
1190 )?;
1191 variants.push((format!("Variant{i}"), ty));
1192 }
1193 out.push(OneOfEnumToEmit { name, variants });
1194 for sub in one_of {
1195 let resolved = resolve_all_of_for_codegen(sub)?;
1196 stack.push((resolved, None));
1197 }
1198 }
1199 for (key, prop_schema) in &node.properties {
1200 stack.push((prop_schema.clone(), Some(key.clone())));
1201 }
1202 }
1203 Ok(out)
1204}
1205
1206fn resolve_ref_for_codegen(
1210 root: &JsonSchema,
1211 schema: &JsonSchema,
1212 fallback_from_key: Option<&str>,
1213) -> CodeGenResult<(JsonSchema, Option<String>)> {
1214 let mut from_key: Option<String> = fallback_from_key.map(String::from);
1215 let Some(ref_str) = schema.ref_.as_deref() else {
1216 return Ok((schema.clone(), from_key));
1217 };
1218
1219 match ref_resolver::parse_ref(ref_str) {
1220 Ok(ref_resolver::ParsedRef::Defs(name) | ref_resolver::ParsedRef::Definitions(name)) => {
1221 from_key = Some(name);
1222 }
1223 Ok(ref_resolver::ParsedRef::Root) => {}
1224 Err(e) => {
1225 return Err(CodeGenError::RefResolution {
1226 ref_str: ref_str.to_string(),
1227 reason: format!("{e:?}"),
1228 });
1229 }
1230 }
1231
1232 let resolved: &JsonSchema =
1233 ref_resolver::resolve_schema_ref_transitive(root, schema).map_err(|e| {
1234 CodeGenError::RefResolution {
1235 ref_str: ref_str.to_string(),
1236 reason: format!("{e:?}"),
1237 }
1238 })?;
1239 Ok((resolved.clone(), from_key))
1240}
1241
1242#[expect(clippy::too_many_lines)]
1243fn collect_structs(
1244 root: &JsonSchema,
1245 schema: &JsonSchema,
1246 from_key: Option<&str>,
1247 out: &mut Vec<StructToEmit>,
1248 seen: &mut BTreeSet<String>,
1249 settings: &CodeGenSettings,
1250) -> CodeGenResult<()> {
1251 let (schema, from_key_opt) = resolve_ref_for_codegen(root, schema, from_key)?;
1252 if !schema.is_object_with_properties() {
1253 return Ok(());
1254 }
1255
1256 let mut post_order: Vec<(JsonSchema, Option<String>, bool)> = Vec::new();
1258 let mut stack: Vec<(JsonSchema, Option<String>, usize, bool)> = Vec::new();
1259 stack.push((
1260 schema.clone(),
1261 from_key_opt.clone(),
1262 0,
1263 from_key_opt.is_none(),
1264 ));
1265
1266 while let Some((schema_node, from_key_opt, index, is_root)) = stack.pop() {
1267 let keys: Vec<String> = schema_node.properties.keys().cloned().collect();
1268 if index < keys.len() {
1269 let key: String = keys.get(index).unwrap().clone();
1270 let child: JsonSchema = schema_node.properties.get(&key).unwrap().clone();
1271 let child_resolved = resolve_all_of_for_codegen(&child)?;
1272 stack.push((schema_node, from_key_opt, index + 1, is_root));
1273 if child_resolved
1274 .any_of
1275 .as_ref()
1276 .is_some_and(|v| !v.is_empty())
1277 {
1278 for (i, sub) in child_resolved.any_of.as_ref().unwrap().iter().enumerate() {
1279 let sub_resolved = resolve_all_of_for_codegen(sub)?;
1280 let variant_key = format!("{key}_Variant{i}");
1281 let (sub_effective, sub_from_key) =
1282 resolve_ref_for_codegen(root, &sub_resolved, Some(&variant_key))?;
1283 if sub_effective.is_object_with_properties() {
1284 stack.push((sub_effective, sub_from_key, 0, false));
1285 } else if sub_resolved.is_array_with_items()
1286 && let Some(ref items) = sub_resolved.items
1287 {
1288 let items_resolved = resolve_all_of_for_codegen(items.as_ref())?;
1289 let (items_effective, items_from_key) =
1290 resolve_ref_for_codegen(root, &items_resolved, Some(&variant_key))?;
1291 if items_effective.is_object_with_properties() {
1292 stack.push((items_effective, items_from_key, 0, false));
1293 }
1294 }
1295 }
1296 } else if child_resolved
1297 .one_of
1298 .as_ref()
1299 .is_some_and(|v| !v.is_empty())
1300 {
1301 for (i, sub) in child_resolved.one_of.as_ref().unwrap().iter().enumerate() {
1302 let sub_resolved = resolve_all_of_for_codegen(sub)?;
1303 let variant_key = format!("{key}_Variant{i}");
1304 let (sub_effective, sub_from_key) =
1305 resolve_ref_for_codegen(root, &sub_resolved, Some(&variant_key))?;
1306 if sub_effective.is_object_with_properties() {
1307 stack.push((sub_effective, sub_from_key, 0, false));
1308 } else if sub_resolved.is_array_with_items()
1309 && let Some(ref items) = sub_resolved.items
1310 {
1311 let items_resolved = resolve_all_of_for_codegen(items.as_ref())?;
1312 let (items_effective, items_from_key) =
1313 resolve_ref_for_codegen(root, &items_resolved, Some(&variant_key))?;
1314 if items_effective.is_object_with_properties() {
1315 stack.push((items_effective, items_from_key, 0, false));
1316 }
1317 }
1318 }
1319 } else {
1320 let (child_effective, child_from_key) =
1321 resolve_ref_for_codegen(root, &child_resolved, Some(&key))?;
1322 if child_effective.is_object_with_properties() {
1323 stack.push((child_effective, child_from_key, 0, false));
1324 } else if child_effective.is_array_with_items()
1325 && let Some(ref items) = child_effective.items
1326 {
1327 let items_resolved = resolve_all_of_for_codegen(items.as_ref())?;
1328 let (items_effective, items_from_key) =
1329 resolve_ref_for_codegen(root, &items_resolved, Some(&key))?;
1330 if items_effective.is_object_with_properties() {
1331 stack.push((items_effective, items_from_key, 0, false));
1332 }
1333 }
1334 }
1335 } else {
1336 post_order.push((schema_node, from_key_opt, is_root));
1337 }
1338 }
1339
1340 for (schema_node, from_key_opt, is_root) in post_order {
1342 let name: String = struct_name_from(
1343 schema_node.title.as_deref(),
1344 from_key_opt.as_deref(),
1345 is_root,
1346 settings,
1347 );
1348
1349 if seen.contains(&name) {
1350 continue;
1351 }
1352 seen.insert(name.clone());
1353
1354 out.push(StructToEmit {
1355 name,
1356 schema: schema_node,
1357 });
1358 }
1359 Ok(())
1360}
1361
1362#[expect(clippy::too_many_lines)]
1365fn collect_structs_all_schemas(
1366 schemas: &[JsonSchema],
1367 settings: &CodeGenSettings,
1368) -> CodeGenResult<Vec<(usize, String, JsonSchema)>> {
1369 let mut out: Vec<(usize, String, JsonSchema)> = Vec::new();
1370 for (schema_idx, schema_root) in schemas.iter().enumerate() {
1371 let (effective_root, root_from_key) =
1372 resolve_ref_for_codegen(schema_root, schema_root, None)?;
1373 if !effective_root.is_object_with_properties() {
1374 continue;
1375 }
1376
1377 let mut post_order: Vec<(JsonSchema, Option<String>, bool)> = Vec::new();
1378 let mut stack: Vec<(JsonSchema, Option<String>, usize, bool)> = Vec::new();
1379 let is_root: bool = root_from_key.is_none();
1380 stack.push((effective_root.clone(), root_from_key, 0, is_root));
1381
1382 while let Some((schema_node, from_key_opt, index, is_root)) = stack.pop() {
1383 let keys: Vec<String> = schema_node.properties.keys().cloned().collect();
1384 if index < keys.len() {
1385 let key: String = keys[index].clone();
1386 let child: JsonSchema = schema_node.properties.get(&key).unwrap().clone();
1387 let child_resolved = resolve_all_of_for_codegen(&child)?;
1388
1389 stack.push((schema_node, from_key_opt, index + 1, is_root));
1390
1391 if child_resolved
1392 .any_of
1393 .as_ref()
1394 .is_some_and(|v| !v.is_empty())
1395 {
1396 for (i, sub) in child_resolved.any_of.as_ref().unwrap().iter().enumerate() {
1397 let sub_resolved = resolve_all_of_for_codegen(sub)?;
1398 let variant_key = format!("{key}_Variant{i}");
1399 let (sub_effective, sub_from_key) = resolve_ref_for_codegen(
1400 schema_root,
1401 &sub_resolved,
1402 Some(&variant_key),
1403 )?;
1404 if sub_effective.is_object_with_properties() {
1405 stack.push((sub_effective, sub_from_key, 0, false));
1406 } else if sub_effective.is_array_with_items()
1407 && let Some(ref items) = sub_effective.items
1408 {
1409 let items_resolved = resolve_all_of_for_codegen(items.as_ref())?;
1410 let (items_effective, items_from_key) = resolve_ref_for_codegen(
1411 schema_root,
1412 &items_resolved,
1413 Some(&variant_key),
1414 )?;
1415 if items_effective.is_object_with_properties() {
1416 stack.push((items_effective, items_from_key, 0, false));
1417 }
1418 }
1419 }
1420 } else if child_resolved
1421 .one_of
1422 .as_ref()
1423 .is_some_and(|v| !v.is_empty())
1424 {
1425 for (i, sub) in child_resolved.one_of.as_ref().unwrap().iter().enumerate() {
1426 let sub_resolved = resolve_all_of_for_codegen(sub)?;
1427 let variant_key = format!("{key}_Variant{i}");
1428 let (sub_effective, sub_from_key) = resolve_ref_for_codegen(
1429 schema_root,
1430 &sub_resolved,
1431 Some(&variant_key),
1432 )?;
1433 if sub_effective.is_object_with_properties() {
1434 stack.push((sub_effective, sub_from_key, 0, false));
1435 } else if sub_effective.is_array_with_items()
1436 && let Some(ref items) = sub_effective.items
1437 {
1438 let items_resolved = resolve_all_of_for_codegen(items.as_ref())?;
1439 let (items_effective, items_from_key) = resolve_ref_for_codegen(
1440 schema_root,
1441 &items_resolved,
1442 Some(&variant_key),
1443 )?;
1444 if items_effective.is_object_with_properties() {
1445 stack.push((items_effective, items_from_key, 0, false));
1446 }
1447 }
1448 }
1449 } else {
1450 let (child_effective, child_from_key) =
1451 resolve_ref_for_codegen(schema_root, &child_resolved, Some(&key))?;
1452 if child_effective.is_object_with_properties() {
1453 stack.push((child_effective, child_from_key, 0, false));
1454 } else if child_effective.is_array_with_items()
1455 && let Some(ref items) = child_effective.items
1456 {
1457 let items_resolved = resolve_all_of_for_codegen(items.as_ref())?;
1458 let (items_effective, items_from_key) =
1459 resolve_ref_for_codegen(schema_root, &items_resolved, Some(&key))?;
1460 if items_effective.is_object_with_properties() {
1461 stack.push((items_effective, items_from_key, 0, false));
1462 }
1463 }
1464 }
1465 } else {
1466 post_order.push((schema_node, from_key_opt, is_root));
1467 }
1468 }
1469
1470 for (schema_node, from_key_opt, is_root) in post_order {
1471 let name: String = struct_name_from(
1472 schema_node.title.as_deref(),
1473 from_key_opt.as_deref(),
1474 is_root,
1475 settings,
1476 );
1477 out.push((schema_idx, name, schema_node));
1478 }
1479 }
1480 Ok(out)
1481}
1482
1483#[expect(clippy::too_many_lines)]
1485#[expect(clippy::type_complexity)]
1486fn generate_rust_with_dedupe(
1487 schemas: &[JsonSchema],
1488 settings: &CodeGenSettings,
1489) -> CodeGenResult<GenerateRustOutput> {
1490 let mode: DedupeMode = settings.dedupe_mode;
1491
1492 let resolved_schemas: Vec<JsonSchema> = schemas
1493 .iter()
1494 .enumerate()
1495 .map(|(i, s)| {
1496 resolve_all_of_for_codegen(s).map_err(|e| CodeGenError::Batch {
1497 index: i,
1498 source: Box::new(e),
1499 })
1500 })
1501 .collect::<CodeGenResult<Vec<_>>>()?;
1502
1503 let mut enum_values_to_name: EnumValuesToNameMap = BTreeMap::new();
1504 for schema in &resolved_schemas {
1505 for e in collect_enums(schema, schema, settings)? {
1506 enum_values_to_name
1507 .entry(e.values.clone())
1508 .or_insert_with(|| (e.name.clone(), e.description.clone(), e.examples.clone()));
1509 }
1510 }
1511 let all_enums: Vec<EnumToEmit> = enum_values_to_name
1512 .iter()
1513 .map(|(values, (name, description, examples))| EnumToEmit {
1514 name: name.clone(),
1515 values: values.clone(),
1516 description: description.clone(),
1517 examples: examples.clone(),
1518 })
1519 .collect();
1520
1521 let collected: Vec<(usize, String, JsonSchema)> =
1522 collect_structs_all_schemas(&resolved_schemas, settings)?;
1523
1524 let mut map: BTreeMap<DedupeKey, (String, JsonSchema, Vec<(usize, String)>)> = BTreeMap::new();
1526 for (schema_idx, name, schema) in &collected {
1527 let key: DedupeKey = DedupeKey::from_schema(schema, mode);
1528 map.entry(key)
1529 .or_insert_with(|| (name.clone(), schema.clone(), Vec::new()))
1530 .2
1531 .push((*schema_idx, name.clone()));
1532 }
1533
1534 let shared_names: BTreeSet<String> = map
1535 .iter()
1536 .filter(|(_, (_, _, occs))| occs.len() > 1)
1537 .map(|(_, (canonical_name, _, _))| canonical_name.clone())
1538 .collect();
1539
1540 let canonical_name_to_first_schema_idx: BTreeMap<String, usize> = {
1541 let mut out: BTreeMap<String, usize> = BTreeMap::new();
1542 for (canonical_name, _, occs) in map.values() {
1543 let first_idx: usize = occs.iter().map(|(i, _)| *i).min().unwrap_or(0);
1544 out.entry(canonical_name.clone())
1545 .and_modify(|v| *v = (*v).min(first_idx))
1546 .or_insert(first_idx);
1547 }
1548 out
1549 };
1550
1551 let key_to_canonical_name: BTreeMap<DedupeKey, String> = map
1552 .iter()
1553 .map(|(k, (canonical, _, _))| (k.clone(), canonical.clone()))
1554 .collect();
1555
1556 let key_to_canonical: BTreeMap<DedupeKey, (String, JsonSchema)> = map
1557 .iter()
1558 .map(|(k, (cn, schema, _))| (k.clone(), (cn.clone(), schema.clone())))
1559 .collect();
1560
1561 if shared_names.is_empty() {
1562 let mut per_schema: Vec<Vec<u8>> = Vec::with_capacity(resolved_schemas.len());
1563 for (index, schema) in resolved_schemas.iter().enumerate() {
1564 let mut out = Cursor::new(Vec::new());
1565 emit_rust(schema, &mut out, settings).map_err(|e| CodeGenError::Batch {
1566 index,
1567 source: Box::new(e),
1568 })?;
1569 per_schema.push({
1570 let result = maybe_prepend_btreemap_use(out.into_inner());
1571 let result = maybe_prepend_hash_set_use(result);
1572 #[cfg(feature = "uuid")]
1573 let result = maybe_prepend_uuid_use(result);
1574 result
1575 });
1576 }
1577 return Ok(GenerateRustOutput {
1578 shared: None,
1579 per_schema,
1580 });
1581 }
1582
1583 let shared_structs: Vec<(String, JsonSchema)> = {
1585 let mut v: Vec<(String, JsonSchema)> = key_to_canonical
1586 .iter()
1587 .filter(|(_, (cn, _))| shared_names.contains(cn))
1588 .map(|(_, (cn, s))| (cn.clone(), s.clone()))
1589 .collect();
1590 let order: Vec<String> = v.iter().map(|(n, _)| n.clone()).collect();
1591 let mut deps: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
1592 for (name, schema) in &v {
1593 let mut set: BTreeSet<String> = BTreeSet::new();
1594 for prop_schema in schema.properties.values() {
1595 if prop_schema.is_object_with_properties() {
1596 let prop_key = DedupeKey::from_schema(prop_schema, mode);
1597 if let Some(cn) = key_to_canonical_name.get(&prop_key)
1598 && shared_names.contains(cn)
1599 {
1600 set.insert(cn.clone());
1601 }
1602 }
1603 if prop_schema.is_array_with_items()
1604 && let Some(ref items) = prop_schema.items
1605 && items.is_object_with_properties()
1606 {
1607 let item_key = DedupeKey::from_schema(items, mode);
1608 if let Some(cn) = key_to_canonical_name.get(&item_key)
1609 && shared_names.contains(cn)
1610 {
1611 set.insert(cn.clone());
1612 }
1613 }
1614 }
1615 deps.insert(name.clone(), set);
1616 }
1617 topo_sort_by_deps(&order, &deps, &mut v);
1618 v
1619 };
1620
1621 let shared_buffer: Vec<u8> = {
1622 let mut out = Cursor::new(Vec::new());
1623 writeln!(
1624 out,
1625 "//! Generated by json-schema-rs. Do not edit manually."
1626 )?;
1627 writeln!(out)?;
1628 writeln!(out, "use serde::{{Deserialize, Serialize}};")?;
1629 writeln!(out)?;
1630 for e in &all_enums {
1631 let pairs: Vec<(String, String)> =
1632 enum_variant_names_with_collision_resolution(&e.values);
1633 emit_enum_from_pairs(
1634 &mut out,
1635 &e.name,
1636 &pairs,
1637 e.description.as_deref(),
1638 e.examples.as_deref(),
1639 )?;
1640 }
1641 for (name, schema) in &shared_structs {
1642 let root_idx: usize = *canonical_name_to_first_schema_idx
1643 .get(name)
1644 .expect("root schema index for shared struct");
1645 let root_schema: &JsonSchema = resolved_schemas.get(root_idx).expect("root schema");
1646 emit_default_functions_for_struct(&mut out, name, schema)?;
1647 emit_struct_derive_and_attrs(&mut out, name, schema)?;
1648 emit_struct_fields_with_resolver(
1649 root_schema,
1650 name,
1651 schema,
1652 &mut out,
1653 settings,
1654 Some(&key_to_canonical_name),
1655 mode,
1656 Some(&enum_values_to_name),
1657 )?;
1658 writeln!(out, "}}")?;
1659 writeln!(out)?;
1660 }
1661 {
1662 let result = maybe_prepend_btreemap_use(out.into_inner());
1663 let result = maybe_prepend_hash_set_use(result);
1664 #[cfg(feature = "uuid")]
1665 let result = maybe_prepend_uuid_use(result);
1666 result
1667 }
1668 };
1669
1670 let per_schema: Vec<Vec<u8>> = (0..schemas.len())
1671 .map(|schema_idx| {
1672 let mut local_structs: Vec<(String, JsonSchema)> = map
1673 .iter()
1674 .filter(|(_, (canonical_name, _, occs))| {
1675 occs.len() == 1
1676 && occs[0].0 == schema_idx
1677 && !shared_names.contains(canonical_name)
1678 })
1679 .map(|(_, (name, schema, _))| (name.clone(), schema.clone()))
1680 .collect();
1681 let local_names: BTreeSet<String> =
1682 local_structs.iter().map(|(n, _)| n.clone()).collect();
1683 let mut deps: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
1684 for (name, schema) in &local_structs {
1685 let mut set: BTreeSet<String> = BTreeSet::new();
1686 for prop_schema in schema.properties.values() {
1687 if prop_schema.is_object_with_properties() {
1688 let prop_key = DedupeKey::from_schema(prop_schema, mode);
1689 if let Some(cn) = key_to_canonical_name.get(&prop_key)
1690 && (local_names.contains(cn) || shared_names.contains(cn))
1691 {
1692 set.insert(cn.clone());
1693 }
1694 }
1695 if prop_schema.is_array_with_items()
1696 && let Some(ref items) = prop_schema.items
1697 && items.is_object_with_properties()
1698 {
1699 let item_key = DedupeKey::from_schema(items, mode);
1700 if let Some(cn) = key_to_canonical_name.get(&item_key)
1701 && (local_names.contains(cn) || shared_names.contains(cn))
1702 {
1703 set.insert(cn.clone());
1704 }
1705 }
1706 }
1707 deps.insert(name.clone(), set);
1708 }
1709 let order: Vec<String> = local_structs.iter().map(|(n, _)| n.clone()).collect();
1710 topo_sort_by_deps(&order, &deps, &mut local_structs);
1711
1712 let mut used_shared: BTreeSet<String> = BTreeSet::new();
1713 for (_, schema) in &local_structs {
1714 for prop_schema in schema.properties.values() {
1715 if prop_schema.is_object_with_properties() {
1716 let prop_key = DedupeKey::from_schema(prop_schema, mode);
1717 if let Some(cn) = key_to_canonical_name.get(&prop_key)
1718 && shared_names.contains(cn)
1719 {
1720 used_shared.insert(cn.clone());
1721 }
1722 }
1723 if prop_schema.is_array_with_items()
1724 && let Some(ref items) = prop_schema.items
1725 && items.is_object_with_properties()
1726 {
1727 let item_key = DedupeKey::from_schema(items, mode);
1728 if let Some(cn) = key_to_canonical_name.get(&item_key)
1729 && shared_names.contains(cn)
1730 {
1731 used_shared.insert(cn.clone());
1732 }
1733 }
1734 if let Some(values) = string_enum_or_const_values(prop_schema)
1735 && let Some((enum_name, _, _)) = enum_values_to_name.get(&values)
1736 {
1737 used_shared.insert(enum_name.clone());
1738 }
1739 }
1740 }
1741 let root_for_schema = collected
1742 .iter()
1743 .rev()
1744 .find(|(idx, _, _)| *idx == schema_idx)
1745 .map(|(_, _, s)| DedupeKey::from_schema(s, mode));
1746 if let Some(root_key) = root_for_schema
1747 && let Some(cn) = key_to_canonical_name.get(&root_key)
1748 && shared_names.contains(cn)
1749 {
1750 used_shared.insert(cn.clone());
1751 }
1752
1753 let mut buf = Cursor::new(Vec::new());
1754 writeln!(
1755 buf,
1756 "//! Generated by json-schema-rs. Do not edit manually."
1757 )
1758 .ok();
1759 writeln!(buf).ok();
1760 writeln!(buf, "use serde::{{Deserialize, Serialize}};").ok();
1761 for u in &used_shared {
1762 writeln!(buf, "pub use crate::{u};").ok();
1763 }
1764 if !used_shared.is_empty() {
1765 writeln!(buf).ok();
1766 }
1767 let root_schema: &JsonSchema = resolved_schemas
1768 .get(schema_idx)
1769 .expect("root schema for local emission");
1770 for (name, schema) in &local_structs {
1771 emit_default_functions_for_struct(&mut buf, name, schema).ok();
1772 emit_struct_derive_and_attrs(&mut buf, name, schema).ok();
1773 emit_struct_fields_with_resolver(
1774 root_schema,
1775 name,
1776 schema,
1777 &mut buf,
1778 settings,
1779 Some(&key_to_canonical_name),
1780 mode,
1781 Some(&enum_values_to_name),
1782 )
1783 .ok();
1784 writeln!(buf, "}}").ok();
1785 writeln!(buf).ok();
1786 }
1787 {
1788 let result = maybe_prepend_btreemap_use(buf.into_inner());
1789 let result = maybe_prepend_hash_set_use(result);
1790 #[cfg(feature = "uuid")]
1791 let result = maybe_prepend_uuid_use(result);
1792 result
1793 }
1794 })
1795 .collect();
1796
1797 Ok(GenerateRustOutput {
1798 shared: Some(shared_buffer),
1799 per_schema,
1800 })
1801}
1802
1803fn topo_sort_by_deps(
1804 order: &[String],
1805 deps: &BTreeMap<String, BTreeSet<String>>,
1806 v: &mut Vec<(String, JsonSchema)>,
1807) {
1808 let mut sorted: Vec<String> = Vec::new();
1809 let mut visited: BTreeSet<String> = BTreeSet::new();
1810 for name in order {
1811 visit_topo(name, deps, &mut visited, &mut sorted);
1812 }
1813 let name_to_pair: BTreeMap<String, (String, JsonSchema)> =
1814 v.drain(..).map(|(n, s)| (n.clone(), (n, s))).collect();
1815 for n in &sorted {
1816 if let Some(pair) = name_to_pair.get(n) {
1817 v.push(pair.clone());
1818 }
1819 }
1820}
1821
1822fn visit_topo(
1823 name: &str,
1824 deps: &BTreeMap<String, BTreeSet<String>>,
1825 visited: &mut BTreeSet<String>,
1826 out: &mut Vec<String>,
1827) {
1828 if visited.contains(name) {
1829 return;
1830 }
1831 visited.insert(name.to_string());
1832 if let Some(d) = deps.get(name) {
1833 for dep in d {
1834 visit_topo(dep, deps, visited, out);
1835 }
1836 }
1837 out.push(name.to_string());
1838}
1839
1840fn emit_enum_from_pairs(
1843 out: &mut impl Write,
1844 name: &str,
1845 pairs: &[(String, String)],
1846 description: Option<&str>,
1847 examples: Option<&[serde_json::Value]>,
1848) -> CodeGenResult<()> {
1849 for line in doc_lines(description) {
1850 writeln!(out, "/// {line}")?;
1851 }
1852 for line in examples_doc_lines(examples) {
1853 writeln!(out, "/// {line}")?;
1854 }
1855 writeln!(
1856 out,
1857 "#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]"
1858 )?;
1859 writeln!(out, "pub enum {name} {{")?;
1860 for (value, variant_name) in pairs {
1861 if value != variant_name {
1862 let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
1863 writeln!(out, " #[serde(rename = \"{escaped}\")]")?;
1864 }
1865 writeln!(out, " {variant_name},")?;
1866 }
1867 writeln!(out, "}}")?;
1868 writeln!(out)?;
1869 Ok(())
1870}
1871
1872fn emit_anyof_enum(out: &mut impl Write, a: &AnyOfEnumToEmit) -> CodeGenResult<()> {
1875 writeln!(out, "#[derive(Debug, Clone, Serialize, Deserialize)]")?;
1876 writeln!(out, "pub enum {} {{", a.name)?;
1877 for (variant_name, ty) in &a.variants {
1878 writeln!(out, " {variant_name}({ty}),")?;
1879 }
1880 writeln!(out, "}}")?;
1881 writeln!(out)?;
1882 Ok(())
1883}
1884
1885fn emit_oneof_enum(out: &mut impl Write, a: &OneOfEnumToEmit) -> CodeGenResult<()> {
1888 writeln!(out, "#[derive(Debug, Clone, Serialize, Deserialize)]")?;
1889 writeln!(out, "pub enum {} {{", a.name)?;
1890 for (variant_name, ty) in &a.variants {
1891 writeln!(out, " {variant_name}({ty}),")?;
1892 }
1893 writeln!(out, "}}")?;
1894 writeln!(out)?;
1895 Ok(())
1896}
1897
1898fn emit_default_attr(
1901 out: &mut impl Write,
1902 struct_name: &str,
1903 field_name: &str,
1904 prop_schema: &JsonSchema,
1905 ty: &str,
1906 is_required: bool,
1907) -> CodeGenResult<()> {
1908 let Some(ref value) = prop_schema.default_value else {
1909 return Ok(());
1910 };
1911 let is_optional: bool = !is_required;
1912 if json_value_equals_rust_type_default(value, ty, is_optional) {
1913 writeln!(out, " #[serde(default)]")?;
1914 return Ok(());
1915 }
1916 let Some(_expr) = json_default_to_rust_expr(value, ty, is_optional) else {
1917 return Ok(());
1918 };
1919 let fn_name: String = default_function_name(struct_name, field_name);
1920 writeln!(out, " #[serde(default = \"{fn_name}\")]")?;
1921 Ok(())
1922}
1923
1924fn emit_default_functions_for_struct(
1926 out: &mut impl Write,
1927 struct_name: &str,
1928 schema: &JsonSchema,
1929) -> CodeGenResult<()> {
1930 for (key, prop_schema) in &schema.properties {
1931 let Some(ref value) = prop_schema.default_value else {
1932 continue;
1933 };
1934 let field_name = sanitize_field_name(key);
1935 let is_required = schema.is_required(key);
1936 let is_optional = !is_required;
1937 let ty: String = if prop_schema.is_string() {
1938 if is_required {
1939 "String".to_string()
1940 } else {
1941 "Option<String>".to_string()
1942 }
1943 } else if prop_schema.is_integer() || prop_schema.is_number() {
1944 let inner = rust_numeric_type_for_schema(prop_schema);
1945 if is_required {
1946 inner
1947 } else {
1948 format!("Option<{inner}>")
1949 }
1950 } else if prop_schema.is_boolean() {
1951 if is_required {
1952 "bool".to_string()
1953 } else {
1954 "Option<bool>".to_string()
1955 }
1956 } else {
1957 continue;
1958 };
1959 if json_value_equals_rust_type_default(value, &ty, is_optional) {
1960 continue;
1961 }
1962 let Some(expr) = json_default_to_rust_expr(value, &ty, is_optional) else {
1963 continue;
1964 };
1965 let fn_name = default_function_name(struct_name, &field_name);
1966 writeln!(out, "fn {fn_name}() -> {ty} {{ {expr} }}")?;
1967 writeln!(out)?;
1968 }
1969 Ok(())
1970}
1971
1972#[expect(clippy::too_many_lines, clippy::too_many_arguments)]
1974fn emit_struct_fields_with_resolver(
1975 root: &JsonSchema,
1976 struct_name: &str,
1977 schema: &JsonSchema,
1978 out: &mut impl Write,
1979 settings: &CodeGenSettings,
1980 key_to_name: Option<&BTreeMap<DedupeKey, String>>,
1981 mode: DedupeMode,
1982 enum_values_to_name: Option<&EnumValuesToNameMap>,
1983) -> CodeGenResult<()> {
1984 let enum_names_simple: Option<BTreeMap<Vec<String>, String>> = enum_values_to_name.map(|m| {
1985 m.iter()
1986 .map(|(k, (n, _, _))| (k.clone(), n.clone()))
1987 .collect()
1988 });
1989 for (key, prop_schema) in &schema.properties {
1990 let (prop_schema_effective, _) = resolve_ref_for_codegen(root, prop_schema, Some(key))?;
1991 let prop_schema: &JsonSchema = &prop_schema_effective;
1992
1993 for line in doc_lines(prop_schema.description.as_deref()) {
1994 writeln!(out, " /// {line}")?;
1995 }
1996 emit_deprecated_attr(out, prop_schema)?;
1997 let field_name = sanitize_field_name(key);
1998 let needs_rename = field_name != *key;
1999
2000 if let Some(values) = string_enum_or_const_values(prop_schema) {
2001 let enum_name: &String = enum_values_to_name
2002 .and_then(|m| m.get(&values).map(|(n, _, _)| n))
2003 .expect("enum name for string enum");
2004 let ty = if schema.is_required(key) {
2005 enum_name.clone()
2006 } else {
2007 format!("Option<{enum_name}>")
2008 };
2009 if needs_rename {
2010 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2011 }
2012 writeln!(out, " pub {field_name}: {ty},")?;
2013 } else if prop_schema.is_string()
2014 || (prop_schema
2015 .enum_values
2016 .as_ref()
2017 .is_some_and(|v| !v.is_empty())
2018 && !prop_schema.is_string_enum())
2019 {
2020 #[cfg(feature = "uuid")]
2021 if prop_schema.is_string() && prop_schema.format.as_deref() == Some("uuid") {
2022 let ty = if schema.is_required(key) {
2023 "Uuid".to_string()
2024 } else {
2025 "Option<Uuid>".to_string()
2026 };
2027 if needs_rename {
2028 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2029 }
2030 writeln!(out, " pub {field_name}: {ty},")?;
2031 continue;
2032 }
2033 let ty = if schema.is_required(key) {
2035 "String".to_string()
2036 } else {
2037 "Option<String>".to_string()
2038 };
2039 if needs_rename {
2040 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2041 }
2042 if prop_schema.min_length.is_some()
2043 || prop_schema.max_length.is_some()
2044 || prop_schema.pattern.is_some()
2045 {
2046 let mut attrs: Vec<String> = Vec::new();
2047 if let Some(n) = prop_schema.min_length {
2048 attrs.push(format!("min_length = {n}"));
2049 }
2050 if let Some(n) = prop_schema.max_length {
2051 attrs.push(format!("max_length = {n}"));
2052 }
2053 if let Some(ref p) = prop_schema.pattern {
2054 let escaped = p.replace('\\', "\\\\").replace('"', "\\\"");
2055 attrs.push(format!("pattern = \"{escaped}\""));
2056 }
2057 writeln!(out, " #[json_schema({})]", attrs.join(", "))?;
2058 }
2059 emit_default_attr(
2060 out,
2061 struct_name,
2062 &field_name,
2063 prop_schema,
2064 &ty,
2065 schema.is_required(key),
2066 )?;
2067 writeln!(out, " pub {field_name}: {ty},")?;
2068 } else if prop_schema.is_integer() || prop_schema.is_number() {
2069 let inner: String = rust_numeric_type_for_schema(prop_schema);
2070 let ty = if schema.is_required(key) {
2071 inner
2072 } else {
2073 format!("Option<{inner}>")
2074 };
2075 if needs_rename {
2076 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2077 }
2078 emit_default_attr(
2079 out,
2080 struct_name,
2081 &field_name,
2082 prop_schema,
2083 &ty,
2084 schema.is_required(key),
2085 )?;
2086 writeln!(out, " pub {field_name}: {ty},")?;
2087 } else if prop_schema.is_boolean() {
2088 let ty: String = if schema.is_required(key) {
2089 "bool".to_string()
2090 } else {
2091 "Option<bool>".to_string()
2092 };
2093 if needs_rename {
2094 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2095 }
2096 emit_default_attr(
2097 out,
2098 struct_name,
2099 &field_name,
2100 prop_schema,
2101 &ty,
2102 schema.is_required(key),
2103 )?;
2104 writeln!(out, " pub {field_name}: {ty},")?;
2105 } else if prop_schema.is_array_with_items() {
2106 let item_schema: &JsonSchema = prop_schema
2107 .items
2108 .as_ref()
2109 .expect("array with items")
2110 .as_ref();
2111 let inner: String = rust_type_for_item_schema(
2112 root,
2113 item_schema,
2114 Some(key),
2115 enum_names_simple.as_ref(),
2116 key_to_name,
2117 settings,
2118 mode,
2119 )?;
2120 let use_hash_set: bool =
2121 prop_schema.unique_items == Some(true) && item_schema_is_hashable(item_schema);
2122 let container: &str = if use_hash_set { "HashSet" } else { "Vec" };
2123 let ty = if schema.is_required(key) {
2124 format!("{container}<{inner}>")
2125 } else {
2126 format!("Option<{container}<{inner}>>")
2127 };
2128 if needs_rename {
2129 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2130 }
2131 if prop_schema.min_items.is_some() || prop_schema.max_items.is_some() {
2132 let mut attrs: Vec<String> = Vec::new();
2133 if let Some(n) = prop_schema.min_items {
2134 attrs.push(format!("min_items = {n}"));
2135 }
2136 if let Some(n) = prop_schema.max_items {
2137 attrs.push(format!("max_items = {n}"));
2138 }
2139 writeln!(out, " #[json_schema({})]", attrs.join(", "))?;
2140 }
2141 writeln!(out, " pub {field_name}: {ty},")?;
2142 } else if prop_schema.is_object_with_properties() {
2143 let nested_name: String = if let Some(m) = key_to_name {
2144 let prop_key = DedupeKey::from_schema(prop_schema, mode);
2145 m.get(&prop_key).cloned().unwrap_or_else(|| {
2146 struct_name_from(prop_schema.title.as_deref(), Some(key), false, settings)
2147 })
2148 } else {
2149 struct_name_from(prop_schema.title.as_deref(), Some(key), false, settings)
2150 };
2151 let ty = if schema.is_required(key) {
2152 nested_name.clone()
2153 } else {
2154 format!("Option<{nested_name}>")
2155 };
2156 if needs_rename {
2157 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2158 }
2159 writeln!(out, " pub {field_name}: {ty},")?;
2160 }
2161 }
2162 if let Some(AdditionalProperties::Schema(sub)) = &schema.additional_properties {
2163 let value_ty: String = rust_type_for_item_schema(
2164 root,
2165 sub,
2166 Some("additional"),
2167 enum_names_simple.as_ref(),
2168 key_to_name,
2169 settings,
2170 mode,
2171 )?;
2172 writeln!(out, " #[serde(default)]")?;
2173 writeln!(out, " pub additional: BTreeMap<String, {value_ty}>,")?;
2174 }
2175 Ok(())
2176}
2177
2178#[expect(clippy::too_many_lines, clippy::too_many_arguments)]
2180fn emit_struct_fields(
2181 root: &JsonSchema,
2182 struct_name: &str,
2183 schema: &JsonSchema,
2184 out: &mut impl Write,
2185 settings: &CodeGenSettings,
2186 enum_values_to_name: Option<&BTreeMap<Vec<String>, String>>,
2187 _anyof_enums: Option<&[AnyOfEnumToEmit]>,
2188 _oneof_enums: Option<&[OneOfEnumToEmit]>,
2189) -> CodeGenResult<()> {
2190 for (key, prop_schema) in &schema.properties {
2191 let (prop_schema_effective, _) = resolve_ref_for_codegen(root, prop_schema, Some(key))?;
2192 let prop_schema: &JsonSchema = &prop_schema_effective;
2193
2194 for line in doc_lines(prop_schema.description.as_deref()) {
2195 writeln!(out, " /// {line}")?;
2196 }
2197 emit_deprecated_attr(out, prop_schema)?;
2198 let field_name = sanitize_field_name(key);
2199 let needs_rename = field_name != *key;
2200
2201 if prop_schema.any_of.as_ref().is_some_and(|v| !v.is_empty()) {
2202 let enum_name = sanitize_struct_name(key) + "AnyOf";
2203 let ty = if schema.is_required(key) {
2204 enum_name.clone()
2205 } else {
2206 format!("Option<{enum_name}>")
2207 };
2208 if needs_rename {
2209 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2210 }
2211 writeln!(out, " pub {field_name}: {ty},")?;
2212 } else if prop_schema.one_of.as_ref().is_some_and(|v| !v.is_empty()) {
2213 let enum_name = sanitize_struct_name(key) + "OneOf";
2214 let ty = if schema.is_required(key) {
2215 enum_name.clone()
2216 } else {
2217 format!("Option<{enum_name}>")
2218 };
2219 if needs_rename {
2220 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2221 }
2222 writeln!(out, " pub {field_name}: {ty},")?;
2223 } else if let Some(values) = string_enum_or_const_values(prop_schema) {
2224 let enum_name: &String = enum_values_to_name
2225 .and_then(|m| m.get(&values))
2226 .expect("enum name for string enum");
2227 let ty = if schema.is_required(key) {
2228 enum_name.clone()
2229 } else {
2230 format!("Option<{enum_name}>")
2231 };
2232 if needs_rename {
2233 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2234 }
2235 writeln!(out, " pub {field_name}: {ty},")?;
2236 } else if prop_schema.is_string()
2237 || (prop_schema
2238 .enum_values
2239 .as_ref()
2240 .is_some_and(|v| !v.is_empty())
2241 && !prop_schema.is_string_enum())
2242 {
2243 #[cfg(feature = "uuid")]
2244 if prop_schema.is_string() && prop_schema.format.as_deref() == Some("uuid") {
2245 let ty = if schema.is_required(key) {
2246 "Uuid".to_string()
2247 } else {
2248 "Option<Uuid>".to_string()
2249 };
2250 if needs_rename {
2251 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2252 }
2253 writeln!(out, " pub {field_name}: {ty},")?;
2254 continue;
2255 }
2256 let ty = if schema.is_required(key) {
2258 "String".to_string()
2259 } else {
2260 "Option<String>".to_string()
2261 };
2262 if needs_rename {
2263 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2264 }
2265 if prop_schema.min_length.is_some()
2266 || prop_schema.max_length.is_some()
2267 || prop_schema.pattern.is_some()
2268 {
2269 let mut attrs: Vec<String> = Vec::new();
2270 if let Some(n) = prop_schema.min_length {
2271 attrs.push(format!("min_length = {n}"));
2272 }
2273 if let Some(n) = prop_schema.max_length {
2274 attrs.push(format!("max_length = {n}"));
2275 }
2276 if let Some(ref p) = prop_schema.pattern {
2277 let escaped = p.replace('\\', "\\\\").replace('"', "\\\"");
2278 attrs.push(format!("pattern = \"{escaped}\""));
2279 }
2280 writeln!(out, " #[json_schema({})]", attrs.join(", "))?;
2281 }
2282 emit_default_attr(
2283 out,
2284 struct_name,
2285 &field_name,
2286 prop_schema,
2287 &ty,
2288 schema.is_required(key),
2289 )?;
2290 writeln!(out, " pub {field_name}: {ty},")?;
2291 } else if prop_schema.is_integer() || prop_schema.is_number() {
2292 let inner: String = rust_numeric_type_for_schema(prop_schema);
2293 let ty = if schema.is_required(key) {
2294 inner
2295 } else {
2296 format!("Option<{inner}>")
2297 };
2298 if needs_rename {
2299 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2300 }
2301 emit_default_attr(
2302 out,
2303 struct_name,
2304 &field_name,
2305 prop_schema,
2306 &ty,
2307 schema.is_required(key),
2308 )?;
2309 writeln!(out, " pub {field_name}: {ty},")?;
2310 } else if prop_schema.is_boolean() {
2311 let ty: String = if schema.is_required(key) {
2312 "bool".to_string()
2313 } else {
2314 "Option<bool>".to_string()
2315 };
2316 if needs_rename {
2317 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2318 }
2319 emit_default_attr(
2320 out,
2321 struct_name,
2322 &field_name,
2323 prop_schema,
2324 &ty,
2325 schema.is_required(key),
2326 )?;
2327 writeln!(out, " pub {field_name}: {ty},")?;
2328 } else if prop_schema.is_array_with_items() {
2329 let item_schema: &JsonSchema = prop_schema
2330 .items
2331 .as_ref()
2332 .expect("array with items")
2333 .as_ref();
2334 let inner: String = rust_type_for_item_schema(
2335 root,
2336 item_schema,
2337 Some(key),
2338 enum_values_to_name,
2339 None,
2340 settings,
2341 DedupeMode::Full,
2342 )?;
2343 let use_hash_set: bool =
2344 prop_schema.unique_items == Some(true) && item_schema_is_hashable(item_schema);
2345 let container: &str = if use_hash_set { "HashSet" } else { "Vec" };
2346 let ty = if schema.is_required(key) {
2347 format!("{container}<{inner}>")
2348 } else {
2349 format!("Option<{container}<{inner}>>")
2350 };
2351 if needs_rename {
2352 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2353 }
2354 if prop_schema.min_items.is_some() || prop_schema.max_items.is_some() {
2355 let mut attrs: Vec<String> = Vec::new();
2356 if let Some(n) = prop_schema.min_items {
2357 attrs.push(format!("min_items = {n}"));
2358 }
2359 if let Some(n) = prop_schema.max_items {
2360 attrs.push(format!("max_items = {n}"));
2361 }
2362 writeln!(out, " #[json_schema({})]", attrs.join(", "))?;
2363 }
2364 writeln!(out, " pub {field_name}: {ty},")?;
2365 } else if prop_schema.is_object_with_properties() {
2366 let nested_name: String =
2367 struct_name_from(prop_schema.title.as_deref(), Some(key), false, settings);
2368 let ty = if schema.is_required(key) {
2369 nested_name.clone()
2370 } else {
2371 format!("Option<{nested_name}>")
2372 };
2373 if needs_rename {
2374 writeln!(out, " #[serde(rename = \"{key}\")]")?;
2375 }
2376 writeln!(out, " pub {field_name}: {ty},")?;
2377 }
2378 }
2379 if let Some(AdditionalProperties::Schema(sub)) = &schema.additional_properties {
2380 let value_ty: String = rust_type_for_item_schema(
2381 root,
2382 sub,
2383 Some("additional"),
2384 enum_values_to_name,
2385 None,
2386 settings,
2387 settings.dedupe_mode,
2388 )?;
2389 writeln!(out, " #[serde(default)]")?;
2390 writeln!(out, " pub additional: BTreeMap<String, {value_ty}>,")?;
2391 }
2392 Ok(())
2393}
2394
2395fn emit_rust(
2397 schema: &JsonSchema,
2398 out: &mut impl Write,
2399 settings: &CodeGenSettings,
2400) -> CodeGenResult<()> {
2401 let root_unresolved = resolve_all_of_for_codegen(schema)?;
2402 let (root, root_from_key) = resolve_ref_for_codegen(schema, &root_unresolved, None)?;
2403 if root.any_of.as_ref().is_some_and(std::vec::Vec::is_empty) {
2404 return Err(CodeGenError::AnyOfEmpty);
2405 }
2406 if root.one_of.as_ref().is_some_and(std::vec::Vec::is_empty) {
2407 return Err(CodeGenError::OneOfEmpty);
2408 }
2409 let roots_for_structs: Vec<JsonSchema> = if root.one_of.as_ref().is_some_and(|v| !v.is_empty())
2410 {
2411 root.one_of
2412 .as_ref()
2413 .unwrap()
2414 .iter()
2415 .map(resolve_all_of_for_codegen)
2416 .collect::<CodeGenResult<Vec<_>>>()?
2417 } else if root.any_of.as_ref().is_some_and(|v| !v.is_empty()) {
2418 root.any_of
2419 .as_ref()
2420 .unwrap()
2421 .iter()
2422 .map(resolve_all_of_for_codegen)
2423 .collect::<CodeGenResult<Vec<_>>>()?
2424 } else {
2425 if !root.is_object_with_properties() {
2426 return Err(CodeGenError::RootNotObject);
2427 }
2428 vec![root.clone()]
2429 };
2430
2431 let enums: Vec<EnumToEmit> = collect_enums(schema, &root, settings)?;
2432 let enum_values_to_name: BTreeMap<Vec<String>, String> = enums
2433 .iter()
2434 .map(|e| (e.values.clone(), e.name.clone()))
2435 .collect();
2436
2437 let anyof_enums: Vec<AnyOfEnumToEmit> =
2438 collect_anyof_enums(schema, &root, settings, &enum_values_to_name)?;
2439 let oneof_enums: Vec<OneOfEnumToEmit> =
2440 collect_oneof_enums(schema, &root, settings, &enum_values_to_name)?;
2441
2442 let mut structs: Vec<StructToEmit> = Vec::new();
2443 let mut seen: BTreeSet<String> = BTreeSet::new();
2444 let root_is_anyof = root.any_of.as_ref().is_some_and(|v| !v.is_empty());
2445 let root_is_oneof = root.one_of.as_ref().is_some_and(|v| !v.is_empty());
2446 for (i, r) in roots_for_structs.iter().enumerate() {
2447 let from_key: Option<String> = if root_is_anyof || root_is_oneof {
2448 Some(format!("Root_Variant{i}"))
2449 } else {
2450 root_from_key.clone()
2451 };
2452 collect_structs(
2453 schema,
2454 r,
2455 from_key.as_deref(),
2456 &mut structs,
2457 &mut seen,
2458 settings,
2459 )?;
2460 }
2461
2462 writeln!(
2463 out,
2464 "//! Generated by json-schema-rs. Do not edit manually."
2465 )?;
2466 writeln!(out)?;
2467 writeln!(out, "use serde::{{Deserialize, Serialize}};")?;
2468 writeln!(out)?;
2469
2470 for e in &enums {
2471 let pairs: Vec<(String, String)> = enum_variant_names_with_collision_resolution(&e.values);
2472 emit_enum_from_pairs(
2473 out,
2474 &e.name,
2475 &pairs,
2476 e.description.as_deref(),
2477 e.examples.as_deref(),
2478 )?;
2479 }
2480
2481 for a in &anyof_enums {
2482 emit_anyof_enum(out, a)?;
2483 }
2484
2485 for o in &oneof_enums {
2486 emit_oneof_enum(out, o)?;
2487 }
2488
2489 for st in &structs {
2490 emit_default_functions_for_struct(out, &st.name, &st.schema)?;
2491 emit_struct_derive_and_attrs(out, &st.name, &st.schema)?;
2492 emit_struct_fields(
2493 schema,
2494 &st.name,
2495 &st.schema,
2496 out,
2497 settings,
2498 Some(&enum_values_to_name),
2499 Some(&anyof_enums),
2500 Some(&oneof_enums),
2501 )?;
2502 writeln!(out, "}}")?;
2503 writeln!(out)?;
2504 }
2505
2506 Ok(())
2507}
2508
2509pub fn generate_rust(
2521 schemas: &[JsonSchema],
2522 settings: &CodeGenSettings,
2523) -> CodeGenResult<GenerateRustOutput> {
2524 RustBackend.generate(schemas, settings)
2525}
2526
2527#[cfg(test)]
2528mod tests {
2529 use super::CodeGenError;
2530 use super::{CodeGenBackend, RustBackend, generate_rust, merge_all_of};
2531 use crate::code_gen::settings::{CodeGenSettings, DedupeMode, ModelNameSource};
2532 use crate::json_schema::JsonSchema;
2533
2534 fn default_settings() -> CodeGenSettings {
2535 CodeGenSettings::builder().build()
2536 }
2537
2538 #[test]
2539 fn root_not_object_errors() {
2540 let schema: JsonSchema = JsonSchema::default();
2541 let settings: CodeGenSettings = default_settings();
2542 let actual = generate_rust(&[schema], &settings).unwrap_err();
2543 assert!(matches!(actual, CodeGenError::Batch { index: 0, .. }));
2544 if let CodeGenError::Batch { source, .. } = actual {
2545 assert!(matches!(*source, CodeGenError::RootNotObject));
2546 }
2547 }
2548
2549 #[test]
2550 fn root_object_empty_properties_errors() {
2551 let schema: JsonSchema = JsonSchema {
2552 type_: Some("object".to_string()),
2553 ..Default::default()
2554 };
2555 let settings: CodeGenSettings = default_settings();
2556 let actual = generate_rust(&[schema], &settings).unwrap_err();
2557 assert!(matches!(actual, CodeGenError::Batch { index: 0, .. }));
2558 if let CodeGenError::Batch { source, .. } = actual {
2559 assert!(matches!(*source, CodeGenError::RootNotObject));
2560 }
2561 }
2562
2563 #[test]
2564 fn schema_with_id_emits_id_attribute() {
2565 let json = r#"{"$id":"http://example.com/schema","type":"object","properties":{"name":{"type":"string"}}}"#;
2566 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2567 let settings: CodeGenSettings = default_settings();
2568 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2569 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2570 let expected = concat!(
2571 "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2572 "use serde::{Deserialize, Serialize};\n\n",
2573 "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2574 "#[json_schema(id = \"http://example.com/schema\")]\n",
2575 "pub struct Root {\n pub name: Option<String>,\n}\n\n"
2576 );
2577 assert_eq!(expected, actual);
2578 }
2579
2580 #[test]
2581 fn additional_properties_false_emits_deny_unknown_fields() {
2582 let json = r#"{"type":"object","properties":{"name":{"type":"string"}},"additionalProperties":false}"#;
2583 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2584 let settings: CodeGenSettings = default_settings();
2585 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2586 let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2587 let expected = concat!(
2588 "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2589 "use serde::{Deserialize, Serialize};\n\n",
2590 "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2591 "#[serde(deny_unknown_fields)]\n",
2592 "pub struct Root {\n pub name: Option<String>,\n}\n\n"
2593 );
2594 assert_eq!(expected, actual);
2595 }
2596
2597 #[test]
2598 fn deprecated_property_emits_deprecated_attr() {
2599 let json =
2600 r#"{"type":"object","properties":{"legacy":{"type":"string","deprecated":true}}}"#;
2601 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2602 let settings: CodeGenSettings = default_settings();
2603 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2604 let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2605 let expected = concat!(
2606 "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2607 "use serde::{Deserialize, Serialize};\n\n",
2608 "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2609 "pub struct Root {\n",
2610 " #[deprecated]\n",
2611 " pub legacy: Option<String>,\n",
2612 "}\n\n"
2613 );
2614 assert_eq!(expected, actual);
2615 }
2616
2617 #[test]
2618 fn deprecated_property_with_description_emits_deprecated_with_message() {
2619 let json = r#"{"type":"object","properties":{"old":{"type":"string","deprecated":true,"description":"Use 'new' instead"}}}"#;
2620 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2621 let settings: CodeGenSettings = default_settings();
2622 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2623 let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2624 let expected = concat!(
2625 "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2626 "use serde::{Deserialize, Serialize};\n\n",
2627 "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2628 "pub struct Root {\n",
2629 " /// Use 'new' instead\n",
2630 " #[deprecated = \"Use 'new' instead\"]\n",
2631 " pub old: Option<String>,\n",
2632 "}\n\n"
2633 );
2634 assert_eq!(expected, actual);
2635 }
2636
2637 #[test]
2638 fn deprecated_false_emits_no_attr() {
2639 let json = r#"{"type":"object","properties":{"x":{"type":"string","deprecated":false}}}"#;
2640 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2641 let settings: CodeGenSettings = default_settings();
2642 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2643 let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2644 let expected = concat!(
2645 "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2646 "use serde::{Deserialize, Serialize};\n\n",
2647 "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2648 "pub struct Root {\n",
2649 " pub x: Option<String>,\n",
2650 "}\n\n"
2651 );
2652 assert_eq!(expected, actual);
2653 }
2654
2655 #[test]
2656 fn additional_properties_schema_emits_map_field() {
2657 let json = r#"{"type":"object","properties":{"name":{"type":"string"}},"additionalProperties":{"type":"string"}}"#;
2658 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2659 let settings: CodeGenSettings = default_settings();
2660 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2661 let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2662 let expected = concat!(
2663 "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2664 "use serde::{Deserialize, Serialize};\n",
2665 "use std::collections::BTreeMap;\n\n",
2666 "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2667 "pub struct Root {\n",
2668 " pub name: Option<String>,\n",
2669 " #[serde(default)]\n",
2670 " pub additional: BTreeMap<String, String>,\n",
2671 "}\n\n"
2672 );
2673 assert_eq!(expected, actual);
2674 }
2675
2676 #[test]
2677 fn single_string_property() {
2678 let json = r#"{"type":"object","properties":{"name":{"type":"string"}}}"#;
2679 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2680 let settings: CodeGenSettings = default_settings();
2681 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2682 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2683 let expected = concat!(
2684 "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2685 "use serde::{Deserialize, Serialize};\n\n",
2686 "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2687 "pub struct Root {\n pub name: Option<String>,\n}\n\n"
2688 );
2689 assert_eq!(expected, actual);
2690 }
2691
2692 #[test]
2693 fn string_property_with_pattern_emits_attribute() {
2694 let json =
2695 r#"{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-z]+$"}}}"#;
2696 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2697 let settings: CodeGenSettings = default_settings();
2698 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2699 let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2700 let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
2701
2702use serde::{Deserialize, Serialize};
2703
2704#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2705pub struct Root {
2706 #[json_schema(pattern = "^[a-z]+$")]
2707 pub name: Option<String>,
2708}
2709
2710"#;
2711 assert_eq!(expected, actual);
2712 }
2713
2714 #[test]
2715 fn required_field_emits_without_option() {
2716 let json = r#"{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}"#;
2717 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2718 let settings: CodeGenSettings = default_settings();
2719 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2720 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2721 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
2722
2723use serde::{Deserialize, Serialize};
2724
2725#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2726pub struct Root {
2727 pub id: String,
2728}
2729
2730";
2731 assert_eq!(expected, actual);
2732 }
2733
2734 #[test]
2735 fn ref_to_defs_object_emits_named_struct_and_field_type() {
2736 let json = r##"{
2737 "$defs": {
2738 "Address": {
2739 "type": "object",
2740 "properties": { "city": { "type": "string" } },
2741 "required": ["city"]
2742 }
2743 },
2744 "type": "object",
2745 "properties": { "address": { "$ref": "#/$defs/Address" } },
2746 "required": ["address"]
2747}"##;
2748 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2749 let settings: CodeGenSettings = default_settings();
2750 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2751 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2752 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
2753
2754use serde::{Deserialize, Serialize};
2755
2756#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2757pub struct Address {
2758 pub city: String,
2759}
2760
2761#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2762pub struct Root {
2763 pub address: Address,
2764}
2765
2766";
2767 assert_eq!(expected, actual);
2768 }
2769
2770 #[test]
2771 fn ref_to_missing_defs_returns_ref_resolution_error() {
2772 let json = r##"{
2773 "type": "object",
2774 "properties": { "x": { "$ref": "#/$defs/Missing" } }
2775}"##;
2776 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2777 let settings: CodeGenSettings = default_settings();
2778 let actual: super::CodeGenResult<super::GenerateRustOutput> =
2779 generate_rust(&[schema], &settings);
2780 assert!(
2781 matches!(
2782 actual,
2783 Err(CodeGenError::RefResolution { ref ref_str, ref reason })
2784 if ref_str == "#/$defs/Missing" && reason.contains("DefsMissing")
2785 ),
2786 "expected RefResolution with DefsMissing, got: {actual:?}"
2787 );
2788 }
2789
2790 #[test]
2791 fn ref_cycle_in_defs_returns_ref_resolution_error() {
2792 let json = r##"{
2793 "$defs": {
2794 "A": { "$ref": "#/$defs/B" },
2795 "B": { "$ref": "#/$defs/A" }
2796 },
2797 "type": "object",
2798 "properties": { "x": { "$ref": "#/$defs/A" } }
2799}"##;
2800 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2801 let settings: CodeGenSettings = default_settings();
2802 let actual: super::CodeGenResult<super::GenerateRustOutput> =
2803 generate_rust(&[schema], &settings);
2804 assert!(
2805 matches!(
2806 actual,
2807 Err(CodeGenError::RefResolution { ref ref_str, ref reason })
2808 if ref_str == "#/$defs/A" && reason.contains("RefCycle")
2809 ),
2810 "expected RefResolution with RefCycle, got: {actual:?}"
2811 );
2812 }
2813
2814 #[test]
2815 fn required_enum_property_emits_enum_and_struct() {
2816 let json = r#"{"type":"object","properties":{"status":{"enum":["open","closed"]}},"required":["status"]}"#;
2817 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2818 let settings: CodeGenSettings = default_settings();
2819 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2820 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2821 let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
2822
2823use serde::{Deserialize, Serialize};
2824
2825#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2826pub enum Status {
2827 #[serde(rename = "closed")]
2828 Closed,
2829 #[serde(rename = "open")]
2830 Open,
2831}
2832
2833#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2834pub struct Root {
2835 pub status: Status,
2836}
2837
2838"#;
2839 assert_eq!(expected, actual);
2840 }
2841
2842 #[test]
2843 fn const_string_property_emits_single_variant_enum() {
2844 let json = r#"{"type":"object","properties":{"key":{"const":"only"}},"required":["key"]}"#;
2845 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2846 let settings: CodeGenSettings = default_settings();
2847 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2848 let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2849 let expected: &str = r#"//! Generated by json-schema-rs. Do not edit manually.
2850
2851use serde::{Deserialize, Serialize};
2852
2853#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2854pub enum Key {
2855 #[serde(rename = "only")]
2856 Only,
2857}
2858
2859#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2860pub struct Root {
2861 pub key: Key,
2862}
2863
2864"#;
2865 assert_eq!(expected, actual);
2866 }
2867
2868 #[test]
2869 fn all_of_merge_same_const_ok() {
2870 let s1: JsonSchema = serde_json::from_str(
2871 r#"{"type":"object","properties":{"x":{"type":"string","const":"same"}}}"#,
2872 )
2873 .unwrap();
2874 let s2: JsonSchema = serde_json::from_str(
2875 r#"{"type":"object","properties":{"x":{"type":"string","const":"same"}}}"#,
2876 )
2877 .unwrap();
2878 let actual: Result<JsonSchema, _> = merge_all_of(&[s1, s2]);
2879 assert!(actual.is_ok());
2880 let merged: JsonSchema = actual.unwrap();
2881 let x_schema: &JsonSchema = merged.properties.get("x").expect("property x");
2882 assert_eq!(
2883 x_schema.const_value.as_ref(),
2884 Some(&serde_json::Value::String("same".to_string()))
2885 );
2886 }
2887
2888 #[test]
2889 fn all_of_merge_conflicting_const_err() {
2890 let s1: JsonSchema = serde_json::from_str(
2891 r#"{"type":"object","properties":{"x":{"type":"string","const":"a"}}}"#,
2892 )
2893 .unwrap();
2894 let s2: JsonSchema = serde_json::from_str(
2895 r#"{"type":"object","properties":{"x":{"type":"string","const":"b"}}}"#,
2896 )
2897 .unwrap();
2898 let actual: Result<JsonSchema, CodeGenError> = merge_all_of(&[s1, s2]);
2899 let err = actual.expect_err("expected AllOfMergeConflictingConst");
2900 assert!(matches!(
2901 err,
2902 CodeGenError::AllOfMergeConflictingConst { .. }
2903 ));
2904 }
2905
2906 #[test]
2907 fn all_of_merge_same_pattern_ok() {
2908 let s1: JsonSchema = serde_json::from_str(
2909 r#"{"type":"object","properties":{"x":{"type":"string","pattern":"^[a-z]+$"}}}"#,
2910 )
2911 .unwrap();
2912 let s2: JsonSchema = serde_json::from_str(
2913 r#"{"type":"object","properties":{"x":{"type":"string","pattern":"^[a-z]+$"}}}"#,
2914 )
2915 .unwrap();
2916 let merged: JsonSchema = merge_all_of(&[s1, s2]).expect("merge ok");
2917 let actual: JsonSchema = merged.properties.get("x").cloned().expect("property x");
2918 let expected: JsonSchema = JsonSchema {
2919 type_: Some("string".to_string()),
2920 pattern: Some("^[a-z]+$".to_string()),
2921 ..Default::default()
2922 };
2923 assert_eq!(expected, actual);
2924 }
2925
2926 #[test]
2927 fn all_of_merge_conflicting_pattern_err() {
2928 let s1: JsonSchema = serde_json::from_str(
2929 r#"{"type":"object","properties":{"x":{"type":"string","pattern":"^[a-z]+$"}}}"#,
2930 )
2931 .unwrap();
2932 let s2: JsonSchema = serde_json::from_str(
2933 r#"{"type":"object","properties":{"x":{"type":"string","pattern":"^[0-9]+$"}}}"#,
2934 )
2935 .unwrap();
2936 let actual: Result<JsonSchema, CodeGenError> = merge_all_of(&[s1, s2]);
2937 assert!(matches!(
2938 actual,
2939 Err(CodeGenError::AllOfMergeConflictingPattern { .. })
2940 ));
2941 }
2942
2943 #[test]
2944 fn all_of_merged_object_golden() {
2945 let json = r#"{"allOf":[{"type":"object","properties":{"a":{"type":"string"}}},{"type":"object","properties":{"b":{"type":"integer"}},"required":["b"]}]}"#;
2946 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2947 let settings = default_settings();
2948 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2949 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2950 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
2951
2952use serde::{Deserialize, Serialize};
2953
2954#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2955pub struct Root {
2956 pub a: Option<String>,
2957 pub b: i64,
2958}
2959
2960";
2961 assert_eq!(expected, actual);
2962 }
2963
2964 #[test]
2965 fn anyof_property_golden() {
2966 let json = r#"{"type":"object","properties":{"foo":{"anyOf":[{"type":"string"},{"type":"object","properties":{"x":{"type":"integer"}}}]}}}"#;
2967 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2968 let settings = default_settings();
2969 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2970 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2971 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
2972
2973use serde::{Deserialize, Serialize};
2974
2975#[derive(Debug, Clone, Serialize, Deserialize)]
2976pub enum FooAnyOf {
2977 Variant0(String),
2978 Variant1(FooVariant1),
2979}
2980
2981#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2982pub struct FooVariant1 {
2983 pub x: Option<i64>,
2984}
2985
2986#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2987pub struct Root {
2988 pub foo: Option<FooAnyOf>,
2989}
2990
2991";
2992 assert_eq!(expected, actual);
2993 }
2994
2995 #[test]
2996 fn anyof_root_golden() {
2997 let json = r#"{"anyOf":[{"type":"object","properties":{"a":{"type":"string"}}},{"type":"object","properties":{"b":{"type":"integer"}}}]}"#;
2998 let schema: JsonSchema = serde_json::from_str(json).unwrap();
2999 let settings = default_settings();
3000 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3001 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3002 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3003
3004use serde::{Deserialize, Serialize};
3005
3006#[derive(Debug, Clone, Serialize, Deserialize)]
3007pub enum RootAnyOf {
3008 Variant0(RootVariant0),
3009 Variant1(RootVariant1),
3010}
3011
3012#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3013pub struct RootVariant0 {
3014 pub a: Option<String>,
3015}
3016
3017#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3018pub struct RootVariant1 {
3019 pub b: Option<i64>,
3020}
3021
3022";
3023 assert_eq!(expected, actual);
3024 }
3025
3026 #[test]
3027 fn anyof_empty_errors() {
3028 let schema: JsonSchema = JsonSchema {
3029 any_of: Some(vec![]),
3030 ..Default::default()
3031 };
3032 let settings = default_settings();
3033 let actual = generate_rust(&[schema], &settings).unwrap_err();
3034 assert!(matches!(actual, CodeGenError::Batch { index: 0, .. }));
3035 if let CodeGenError::Batch { source, .. } = actual {
3036 assert!(matches!(*source, CodeGenError::AnyOfEmpty));
3037 }
3038 }
3039
3040 #[test]
3041 fn oneof_property_golden() {
3042 let json = r#"{"type":"object","properties":{"foo":{"oneOf":[{"type":"string"},{"type":"object","properties":{"x":{"type":"integer"}}}]}}}"#;
3043 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3044 let settings = default_settings();
3045 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3046 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3047 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3048
3049use serde::{Deserialize, Serialize};
3050
3051#[derive(Debug, Clone, Serialize, Deserialize)]
3052pub enum FooOneOf {
3053 Variant0(String),
3054 Variant1(FooVariant1),
3055}
3056
3057#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3058pub struct FooVariant1 {
3059 pub x: Option<i64>,
3060}
3061
3062#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3063pub struct Root {
3064 pub foo: Option<FooOneOf>,
3065}
3066
3067";
3068 assert_eq!(expected, actual);
3069 }
3070
3071 #[test]
3072 fn oneof_root_golden() {
3073 let json = r#"{"oneOf":[{"type":"object","properties":{"a":{"type":"string"}}},{"type":"object","properties":{"b":{"type":"integer"}}}]}"#;
3074 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3075 let settings = default_settings();
3076 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3077 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3078 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3079
3080use serde::{Deserialize, Serialize};
3081
3082#[derive(Debug, Clone, Serialize, Deserialize)]
3083pub enum RootOneOf {
3084 Variant0(RootVariant0),
3085 Variant1(RootVariant1),
3086}
3087
3088#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3089pub struct RootVariant0 {
3090 pub a: Option<String>,
3091}
3092
3093#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3094pub struct RootVariant1 {
3095 pub b: Option<i64>,
3096}
3097
3098";
3099 assert_eq!(expected, actual);
3100 }
3101
3102 #[test]
3103 fn oneof_empty_errors() {
3104 let schema: JsonSchema = JsonSchema {
3105 one_of: Some(vec![]),
3106 ..Default::default()
3107 };
3108 let settings = default_settings();
3109 let actual = generate_rust(&[schema], &settings).unwrap_err();
3110 assert!(matches!(actual, CodeGenError::Batch { index: 0, .. }));
3111 if let CodeGenError::Batch { source, .. } = actual {
3112 assert!(matches!(*source, CodeGenError::OneOfEmpty));
3113 }
3114 }
3115
3116 #[test]
3117 fn merge_all_of_success_two_object_subschemas() {
3118 let s1: JsonSchema =
3119 serde_json::from_str(r#"{"type":"object","properties":{"a":{"type":"string"}}}"#)
3120 .unwrap();
3121 let s2: JsonSchema = serde_json::from_str(
3122 r#"{"type":"object","properties":{"b":{"type":"integer"}},"required":["b"]}"#,
3123 )
3124 .unwrap();
3125 let actual = merge_all_of(&[s1, s2]).unwrap();
3126 let expected: JsonSchema = serde_json::from_str(
3127 r#"{"type":"object","properties":{"a":{"type":"string"},"b":{"type":"integer"}},"required":["b"]}"#,
3128 )
3129 .unwrap();
3130 assert_eq!(expected, actual);
3131 }
3132
3133 #[test]
3134 fn merge_all_of_empty_errors() {
3135 let actual = merge_all_of(&[]);
3136 assert!(matches!(actual, Err(CodeGenError::AllOfMergeEmpty)));
3137 }
3138
3139 #[test]
3140 fn merge_all_of_single_schema() {
3141 let s: JsonSchema =
3142 serde_json::from_str(r#"{"type":"object","properties":{"x":{"type":"string"}}}"#)
3143 .unwrap();
3144 let expected = s.clone();
3145 let actual = merge_all_of(std::slice::from_ref(&s)).unwrap();
3146 assert_eq!(expected, actual);
3147 }
3148
3149 #[test]
3150 fn merge_all_of_conflicting_property_type_errors() {
3151 let s1: JsonSchema =
3152 serde_json::from_str(r#"{"type":"object","properties":{"k":{"type":"string"}}}"#)
3153 .unwrap();
3154 let s2: JsonSchema =
3155 serde_json::from_str(r#"{"type":"object","properties":{"k":{"type":"integer"}}}"#)
3156 .unwrap();
3157 let actual = merge_all_of(&[s1, s2]);
3158 assert!(matches!(
3159 actual,
3160 Err(CodeGenError::AllOfMergeConflictingPropertyType {
3161 property_key: ref k,
3162 ..
3163 }) if k == "k"
3164 ));
3165 }
3166
3167 #[test]
3168 fn merge_all_of_non_object_subschema_errors() {
3169 let s1: JsonSchema =
3170 serde_json::from_str(r#"{"type":"object","properties":{"a":{"type":"string"}}}"#)
3171 .unwrap();
3172 let s2: JsonSchema = serde_json::from_str(r#"{"type":"string"}"#).unwrap();
3173 let actual = merge_all_of(&[s1, s2]);
3174 assert!(matches!(
3175 actual,
3176 Err(CodeGenError::AllOfMergeNonObjectSubschema { index: 1 })
3177 ));
3178 }
3179
3180 #[test]
3181 fn merge_all_of_conflicting_enum_errors() {
3182 let s1: JsonSchema =
3183 serde_json::from_str(r#"{"type":"object","properties":{"s":{"enum":["a","b"]}}}"#)
3184 .unwrap();
3185 let s2: JsonSchema =
3186 serde_json::from_str(r#"{"type":"object","properties":{"s":{"enum":["x","y"]}}}"#)
3187 .unwrap();
3188 let actual = merge_all_of(&[s1, s2]);
3189 assert!(matches!(
3190 actual,
3191 Err(CodeGenError::AllOfMergeConflictingEnum { property_key: ref k }) if k == "s"
3192 ));
3193 }
3194
3195 #[test]
3196 fn merge_all_of_conflicting_numeric_bounds_errors() {
3197 let s1: JsonSchema = serde_json::from_str(
3198 r#"{"type":"object","properties":{"n":{"type":"integer","minimum":0,"maximum":10}}}"#,
3199 )
3200 .unwrap();
3201 let s2: JsonSchema = serde_json::from_str(
3202 r#"{"type":"object","properties":{"n":{"type":"integer","minimum":20,"maximum":30}}}"#,
3203 )
3204 .unwrap();
3205 let actual = merge_all_of(&[s1, s2]);
3206 assert!(matches!(
3207 actual,
3208 Err(CodeGenError::AllOfMergeConflictingNumericBounds {
3209 property_key: ref k,
3210 keyword: ref w
3211 }) if k == "n" && w == "minimum/maximum"
3212 ));
3213 }
3214
3215 #[test]
3216 fn batch_error_when_allof_merge_fails_in_second_schema() {
3217 let s0: JsonSchema =
3218 serde_json::from_str(r#"{"type":"object","properties":{"a":{"type":"string"}}}"#)
3219 .unwrap();
3220 let s1_bad: JsonSchema = serde_json::from_str(
3221 r#"{"allOf":[{"type":"object","properties":{"x":{"type":"string"}}},{"type":"object","properties":{"x":{"type":"integer"}}}]}"#,
3222 )
3223 .unwrap();
3224 let settings = default_settings();
3225 let actual = generate_rust(&[s0.clone(), s1_bad], &settings).unwrap_err();
3226 assert!(matches!(actual, CodeGenError::Batch { index: 1, .. }));
3227 }
3228
3229 #[test]
3230 fn root_all_of_merges_to_empty_object_errors_with_root_not_object() {
3231 let schema: JsonSchema =
3232 serde_json::from_str(r#"{"allOf":[{"type":"object"},{"type":"object"}]}"#).unwrap();
3233 let settings = default_settings();
3234 let actual = generate_rust(&[schema], &settings).unwrap_err();
3235 assert!(
3236 matches!(actual, CodeGenError::Batch { index: 0, source: ref s } if matches!(**s, CodeGenError::RootNotObject)),
3237 "expected Batch {{ index: 0, source: RootNotObject }}, got {actual:?}"
3238 );
3239 }
3240
3241 #[test]
3242 fn optional_enum_property_emits_enum_and_struct() {
3243 let json = r#"{"type":"object","properties":{"level":{"enum":["low","medium","high"]}}}"#;
3244 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3245 let settings: CodeGenSettings = default_settings();
3246 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3247 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3248 let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3249
3250use serde::{Deserialize, Serialize};
3251
3252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3253pub enum Level {
3254 #[serde(rename = "high")]
3255 High,
3256 #[serde(rename = "low")]
3257 Low,
3258 #[serde(rename = "medium")]
3259 Medium,
3260}
3261
3262#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3263pub struct Root {
3264 pub level: Option<Level>,
3265}
3266
3267"#;
3268 assert_eq!(expected, actual);
3269 }
3270
3271 #[test]
3272 fn enum_dedupe_two_properties_same_enum_emits_one_enum() {
3273 let json = r#"{"type":"object","properties":{"a":{"enum":["x","y"]},"b":{"enum":["x","y"]}},"required":["a"]}"#;
3274 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3275 let settings: CodeGenSettings = default_settings();
3276 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3277 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3278 let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3279
3280use serde::{Deserialize, Serialize};
3281
3282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3283pub enum A {
3284 #[serde(rename = "x")]
3285 X,
3286 #[serde(rename = "y")]
3287 Y,
3288}
3289
3290#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3291pub struct Root {
3292 pub a: A,
3293 pub b: Option<A>,
3294}
3295
3296"#;
3297 assert_eq!(expected, actual);
3298 }
3299
3300 #[test]
3301 fn enum_collision_emits_suffixed_variants() {
3302 let json = r#"{"type":"object","properties":{"t":{"enum":["a","A"]}},"required":["t"]}"#;
3303 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3304 let settings: CodeGenSettings = default_settings();
3305 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3306 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3307 let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3308
3309use serde::{Deserialize, Serialize};
3310
3311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3312pub enum T {
3313 #[serde(rename = "A")]
3314 A0,
3315 #[serde(rename = "a")]
3316 A1,
3317}
3318
3319#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3320pub struct Root {
3321 pub t: T,
3322}
3323
3324"#;
3325 assert_eq!(expected, actual);
3326 }
3327
3328 #[test]
3329 fn enum_duplicate_values_in_schema_emits_single_variant_per_value() {
3330 let json = r#"{"type":"object","properties":{"t":{"enum":["A","A","A","a","a","a","a","a","a","a"]}},"required":["t"]}"#;
3331 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3332 let settings: CodeGenSettings = default_settings();
3333 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3334 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3335 let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3336
3337use serde::{Deserialize, Serialize};
3338
3339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3340pub enum T {
3341 #[serde(rename = "A")]
3342 A0,
3343 #[serde(rename = "a")]
3344 A1,
3345}
3346
3347#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3348pub struct Root {
3349 pub t: T,
3350}
3351
3352"#;
3353 assert_eq!(expected, actual);
3354 }
3355
3356 #[test]
3357 fn enum_non_rust_compliant_values_emits_valid_variants() {
3358 let json = r#"{"type":"object","properties":{"id":{"enum":["/8633","todd.griffin"]}},"required":["id"]}"#;
3359 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3360 let settings: CodeGenSettings = default_settings();
3361 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3362 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3363 let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3364
3365use serde::{Deserialize, Serialize};
3366
3367#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3368pub enum Id {
3369 #[serde(rename = "/8633")]
3370 E8633,
3371 #[serde(rename = "todd.griffin")]
3372 ToddGriffin,
3373}
3374
3375#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3376pub struct Root {
3377 pub id: Id,
3378}
3379
3380"#;
3381 assert_eq!(expected, actual);
3382 }
3383
3384 #[test]
3385 fn camel_case_property_emits_snake_case_field() {
3386 let json = r#"{"type":"object","properties":{"toddGriffin":{"type":"string"}},"required":["toddGriffin"]}"#;
3387 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3388 let settings: CodeGenSettings = default_settings();
3389 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3390 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3391 let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3392
3393use serde::{Deserialize, Serialize};
3394
3395#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3396pub struct Root {
3397 #[serde(rename = "toddGriffin")]
3398 pub todd_griffin: String,
3399}
3400
3401"#;
3402 assert_eq!(expected, actual);
3403 }
3404
3405 #[test]
3406 fn default_function_name_uses_snake_case() {
3407 let json = r#"{"type":"object","properties":{"person":{"title":"ValerieHunter","type":"object","properties":{"toddGriffin":{"type":"string","default":"bar"}},"required":["toddGriffin"]}},"required":["person"]}"#;
3408 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3409 let settings: CodeGenSettings = default_settings();
3410 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3411 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3412 let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3413
3414use serde::{Deserialize, Serialize};
3415
3416fn default_valerie_hunter_todd_griffin() -> String { "bar".to_string() }
3417
3418#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3419#[json_schema(title = "ValerieHunter")]
3420pub struct ValerieHunter {
3421 #[serde(rename = "toddGriffin")]
3422 #[serde(default = "default_valerie_hunter_todd_griffin")]
3423 pub todd_griffin: String,
3424}
3425
3426#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3427pub struct Root {
3428 pub person: ValerieHunter,
3429}
3430
3431"#;
3432 assert_eq!(expected, actual);
3433 }
3434
3435 #[test]
3436 fn non_string_enum_fallback_emits_string() {
3437 let json =
3438 r#"{"type":"object","properties":{"tag":{"enum":["foo",1,true]}},"required":["tag"]}"#;
3439 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3440 let settings: CodeGenSettings = default_settings();
3441 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3442 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3443 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3444
3445use serde::{Deserialize, Serialize};
3446
3447#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3448pub struct Root {
3449 pub tag: String,
3450}
3451
3452";
3453 assert_eq!(expected, actual);
3454 }
3455
3456 #[test]
3457 fn single_required_integer_property() {
3458 let json =
3459 r#"{"type":"object","properties":{"count":{"type":"integer"}},"required":["count"]}"#;
3460 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3461 let settings: CodeGenSettings = default_settings();
3462 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3463 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3464 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3465
3466use serde::{Deserialize, Serialize};
3467
3468#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3469pub struct Root {
3470 pub count: i64,
3471}
3472
3473";
3474 assert_eq!(expected, actual);
3475 }
3476
3477 #[test]
3478 fn single_optional_integer_property() {
3479 let json = r#"{"type":"object","properties":{"count":{"type":"integer"}}}"#;
3480 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3481 let settings: CodeGenSettings = default_settings();
3482 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3483 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3484 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3485
3486use serde::{Deserialize, Serialize};
3487
3488#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3489pub struct Root {
3490 pub count: Option<i64>,
3491}
3492
3493";
3494 assert_eq!(expected, actual);
3495 }
3496
3497 #[test]
3498 fn single_required_float_property() {
3499 let json =
3500 r#"{"type":"object","properties":{"value":{"type":"number"}},"required":["value"]}"#;
3501 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3502 let settings: CodeGenSettings = default_settings();
3503 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3504 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3505 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3506
3507use serde::{Deserialize, Serialize};
3508
3509#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3510pub struct Root {
3511 pub value: f64,
3512}
3513
3514";
3515 assert_eq!(expected, actual);
3516 }
3517
3518 #[test]
3519 fn single_optional_float_property() {
3520 let json = r#"{"type":"object","properties":{"value":{"type":"number"}}}"#;
3521 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3522 let settings: CodeGenSettings = default_settings();
3523 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3524 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3525 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3526
3527use serde::{Deserialize, Serialize};
3528
3529#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3530pub struct Root {
3531 pub value: Option<f64>,
3532}
3533
3534";
3535 assert_eq!(expected, actual);
3536 }
3537
3538 #[test]
3539 fn integer_with_minimum_maximum_u8_range_emits_u8() {
3540 let json = r#"{"type":"object","properties":{"byte":{"type":"integer","minimum":0,"maximum":255}},"required":["byte"]}"#;
3541 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3542 let settings: CodeGenSettings = default_settings();
3543 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3544 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3545 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3546
3547use serde::{Deserialize, Serialize};
3548
3549#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3550pub struct Root {
3551 pub byte: u8,
3552}
3553
3554";
3555 assert_eq!(expected, actual);
3556 }
3557
3558 #[test]
3559 fn integer_with_only_minimum_emits_i64_fallback() {
3560 let json = r#"{"type":"object","properties":{"count":{"type":"integer","minimum":0}}}"#;
3561 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3562 let settings: CodeGenSettings = default_settings();
3563 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3564 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3565 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3566
3567use serde::{Deserialize, Serialize};
3568
3569#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3570pub struct Root {
3571 pub count: Option<i64>,
3572}
3573
3574";
3575 assert_eq!(expected, actual);
3576 }
3577
3578 #[test]
3579 fn integer_with_only_maximum_emits_i64_fallback() {
3580 let json = r#"{"type":"object","properties":{"count":{"type":"integer","maximum":100}}}"#;
3581 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3582 let settings: CodeGenSettings = default_settings();
3583 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3584 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3585 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3586
3587use serde::{Deserialize, Serialize};
3588
3589#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3590pub struct Root {
3591 pub count: Option<i64>,
3592}
3593
3594";
3595 assert_eq!(expected, actual);
3596 }
3597
3598 #[test]
3599 fn number_without_min_max_emits_f64_fallback() {
3600 let json =
3601 r#"{"type":"object","properties":{"value":{"type":"number"}},"required":["value"]}"#;
3602 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3603 let settings: CodeGenSettings = default_settings();
3604 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3605 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3606 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3607
3608use serde::{Deserialize, Serialize};
3609
3610#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3611pub struct Root {
3612 pub value: f64,
3613}
3614
3615";
3616 assert_eq!(expected, actual);
3617 }
3618
3619 #[test]
3620 fn number_with_minimum_maximum_f32_range_emits_f32() {
3621 let json = r#"{"type":"object","properties":{"value":{"type":"number","minimum":0.5,"maximum":100.5}},"required":["value"]}"#;
3622 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3623 let settings: CodeGenSettings = default_settings();
3624 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3625 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3626 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3627
3628use serde::{Deserialize, Serialize};
3629
3630#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3631pub struct Root {
3632 pub value: f32,
3633}
3634
3635";
3636 assert_eq!(expected, actual);
3637 }
3638
3639 #[test]
3640 fn boolean_property_required_emits_bool() {
3641 let json = r#"{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}"#;
3642 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3643 let settings: CodeGenSettings = default_settings();
3644 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3645 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3646 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3647
3648use serde::{Deserialize, Serialize};
3649
3650#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3651pub struct Root {
3652 pub enabled: bool,
3653}
3654
3655";
3656 assert_eq!(expected, actual);
3657 }
3658
3659 #[test]
3660 fn boolean_property_optional_emits_option_bool() {
3661 let json = r#"{"type":"object","properties":{"flag":{"type":"boolean"}}}"#;
3662 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3663 let settings: CodeGenSettings = default_settings();
3664 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3665 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3666 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3667
3668use serde::{Deserialize, Serialize};
3669
3670#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3671pub struct Root {
3672 pub flag: Option<bool>,
3673}
3674
3675";
3676 assert_eq!(expected, actual);
3677 }
3678
3679 #[test]
3680 fn boolean_property_with_description_emits_doc_and_field() {
3681 let json = r#"{"type":"object","properties":{"flag":{"type":"boolean","description":"Is it enabled?"}}}"#;
3682 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3683 let settings: CodeGenSettings = default_settings();
3684 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3685 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3686 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3687
3688use serde::{Deserialize, Serialize};
3689
3690#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3691pub struct Root {
3692 /// Is it enabled?
3693 pub flag: Option<bool>,
3694}
3695
3696";
3697 assert_eq!(expected, actual);
3698 }
3699
3700 #[test]
3701 fn array_items_boolean_emits_vec_bool() {
3702 let json = r#"{"type":"object","properties":{"flags":{"type":"array","items":{"type":"boolean"}}},"required":["flags"]}"#;
3703 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3704 let settings: CodeGenSettings = default_settings();
3705 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3706 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3707 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3708
3709use serde::{Deserialize, Serialize};
3710
3711#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3712pub struct Root {
3713 pub flags: Vec<bool>,
3714}
3715
3716";
3717 assert_eq!(expected, actual);
3718 }
3719
3720 #[test]
3721 fn array_items_boolean_unique_items_emits_hash_set_bool() {
3722 let json = r#"{"type":"object","properties":{"flags":{"type":"array","items":{"type":"boolean"},"uniqueItems":true}},"required":["flags"]}"#;
3723 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3724 let settings: CodeGenSettings = default_settings();
3725 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3726 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3727 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3728
3729use serde::{Deserialize, Serialize};
3730use std::collections::HashSet;
3731
3732#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3733pub struct Root {
3734 pub flags: HashSet<bool>,
3735}
3736
3737";
3738 assert_eq!(expected, actual);
3739 }
3740
3741 #[test]
3742 fn boolean_property_with_default_emits_default_attr() {
3743 let json =
3744 r#"{"type":"object","properties":{"enabled":{"type":"boolean","default":true}}}"#;
3745 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3746 let settings: CodeGenSettings = default_settings();
3747 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3748 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3749 let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3750
3751use serde::{Deserialize, Serialize};
3752
3753fn default_root_enabled() -> Option<bool> { Some(true) }
3754
3755#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3756pub struct Root {
3757 #[serde(default = "default_root_enabled")]
3758 pub enabled: Option<bool>,
3759}
3760
3761"#;
3762 assert_eq!(expected, actual);
3763 }
3764
3765 #[test]
3766 fn array_required_string_property() {
3767 let json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}}},"required":["tags"]}"#;
3768 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3769 let settings: CodeGenSettings = default_settings();
3770 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3771 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3772 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3773
3774use serde::{Deserialize, Serialize};
3775
3776#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3777pub struct Root {
3778 pub tags: Vec<String>,
3779}
3780
3781";
3782 assert_eq!(expected, actual);
3783 }
3784
3785 #[test]
3786 fn array_with_unique_items_true_emits_hash_set_string() {
3787 let json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"uniqueItems":true}},"required":["tags"]}"#;
3788 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3789 let settings: CodeGenSettings = default_settings();
3790 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3791 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3792 assert!(
3793 actual.contains("pub tags: HashSet<String>"),
3794 "expected HashSet<String>: {actual}"
3795 );
3796 assert!(
3797 actual.contains(concat!("use std::collections::", "HashSet")),
3798 "expected HashSet use: {actual}"
3799 );
3800 }
3801
3802 #[test]
3803 fn array_with_unique_items_false_emits_vec_string() {
3804 let json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"uniqueItems":false}},"required":["tags"]}"#;
3805 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3806 let settings: CodeGenSettings = default_settings();
3807 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3808 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3809 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3810
3811use serde::{Deserialize, Serialize};
3812
3813#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3814pub struct Root {
3815 pub tags: Vec<String>,
3816}
3817
3818";
3819 assert_eq!(expected, actual);
3820 }
3821
3822 #[test]
3823 fn array_with_unique_items_true_object_items_emits_vec() {
3824 let json = r#"{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}}},"uniqueItems":true}},"required":["items"]}"#;
3825 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3826 let settings: CodeGenSettings = default_settings();
3827 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3828 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3829 assert!(
3830 actual.contains("pub items: Vec<") && actual.contains(">,"),
3831 "uniqueItems true with object items should emit Vec: {actual}"
3832 );
3833 assert!(
3834 !actual.contains("HashSet"),
3835 "should not use HashSet for object items: {actual}"
3836 );
3837 }
3838
3839 #[test]
3840 fn array_with_min_items_max_items_emits_attribute() {
3841 let json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":5}},"required":["tags"]}"#;
3842 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3843 let settings: CodeGenSettings = default_settings();
3844 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3845 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3846 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3847
3848use serde::{Deserialize, Serialize};
3849
3850#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3851pub struct Root {
3852 #[json_schema(min_items = 2, max_items = 5)]
3853 pub tags: Vec<String>,
3854}
3855
3856";
3857 assert_eq!(expected, actual);
3858 }
3859
3860 #[test]
3861 fn array_with_min_items_only_emits_attribute() {
3862 let json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"minItems":1}},"required":["tags"]}"#;
3863 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3864 let settings: CodeGenSettings = default_settings();
3865 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3866 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3867 assert!(
3868 actual.contains("#[json_schema(min_items = 1)]"),
3869 "expected min_items attribute: {actual}"
3870 );
3871 assert!(
3872 !actual.contains("max_items"),
3873 "should not emit max_items when absent: {actual}"
3874 );
3875 }
3876
3877 #[test]
3878 fn array_optional_string_property() {
3879 let json =
3880 r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}}}}"#;
3881 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3882 let settings: CodeGenSettings = default_settings();
3883 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3884 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3885 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3886
3887use serde::{Deserialize, Serialize};
3888
3889#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3890pub struct Root {
3891 pub tags: Option<Vec<String>>,
3892}
3893
3894";
3895 assert_eq!(expected, actual);
3896 }
3897
3898 #[test]
3899 fn array_of_integers_property() {
3900 let json = r#"{"type":"object","properties":{"counts":{"type":"array","items":{"type":"integer"}}},"required":["counts"]}"#;
3901 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3902 let settings: CodeGenSettings = default_settings();
3903 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3904 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3905 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3906
3907use serde::{Deserialize, Serialize};
3908
3909#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3910pub struct Root {
3911 pub counts: Vec<i64>,
3912}
3913
3914";
3915 assert_eq!(expected, actual);
3916 }
3917
3918 #[test]
3919 fn array_of_objects_property() {
3920 let json = r#"{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}}}}},"required":["items"]}"#;
3921 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3922 let settings: CodeGenSettings = default_settings();
3923 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3924 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3925 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3926
3927use serde::{Deserialize, Serialize};
3928
3929#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3930pub struct Items {
3931 pub name: Option<String>,
3932}
3933
3934#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3935pub struct Root {
3936 pub items: Vec<Items>,
3937}
3938
3939";
3940 assert_eq!(expected, actual);
3941 }
3942
3943 #[test]
3944 fn array_of_arrays_property() {
3945 let json = r#"{"type":"object","properties":{"matrix":{"type":"array","items":{"type":"array","items":{"type":"string"}}}},"required":["matrix"]}"#;
3946 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3947 let settings: CodeGenSettings = default_settings();
3948 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3949 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3950 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3951
3952use serde::{Deserialize, Serialize};
3953
3954#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3955pub struct Root {
3956 pub matrix: Vec<Vec<String>>,
3957}
3958
3959";
3960 assert_eq!(expected, actual);
3961 }
3962
3963 #[test]
3964 fn mixed_string_integer_float_properties() {
3965 let json = r#"{"type":"object","properties":{"id":{"type":"string"},"count":{"type":"integer"},"value":{"type":"number"}},"required":["id"]}"#;
3966 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3967 let settings: CodeGenSettings = default_settings();
3968 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3969 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3970 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3971
3972use serde::{Deserialize, Serialize};
3973
3974#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3975pub struct Root {
3976 pub count: Option<i64>,
3977 pub id: String,
3978 pub value: Option<f64>,
3979}
3980
3981";
3982 assert_eq!(expected, actual);
3983 }
3984
3985 #[test]
3986 fn mixed_string_and_integer_properties() {
3987 let json = r#"{"type":"object","properties":{"id":{"type":"string"},"count":{"type":"integer"}},"required":["id"]}"#;
3988 let schema: JsonSchema = serde_json::from_str(json).unwrap();
3989 let settings: CodeGenSettings = default_settings();
3990 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3991 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3992 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3993
3994use serde::{Deserialize, Serialize};
3995
3996#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3997pub struct Root {
3998 pub count: Option<i64>,
3999 pub id: String,
4000}
4001
4002";
4003 assert_eq!(expected, actual);
4004 }
4005
4006 #[test]
4007 fn nested_object_and_rename() {
4008 let json = r#"{
4009 "type": "object",
4010 "properties": {
4011 "first_name": { "type": "string" },
4012 "address": {
4013 "type": "object",
4014 "properties": {
4015 "street_address": { "type": "string" },
4016 "city": { "type": "string" }
4017 }
4018 }
4019 }
4020 }"#;
4021 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4022 let settings: CodeGenSettings = default_settings();
4023 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4024 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4025 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4026
4027use serde::{Deserialize, Serialize};
4028
4029#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4030pub struct Address {
4031 pub city: Option<String>,
4032 pub street_address: Option<String>,
4033}
4034
4035#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4036pub struct Root {
4037 pub address: Option<Address>,
4038 pub first_name: Option<String>,
4039}
4040
4041";
4042 assert_eq!(expected, actual);
4043 }
4044
4045 #[test]
4046 fn full_example_from_plan() {
4047 let json = r#"{
4048 "type": "object",
4049 "properties": {
4050 "first_name": { "type": "string" },
4051 "last_name": { "type": "string" },
4052 "birthday": { "type": "string" },
4053 "address": {
4054 "type": "object",
4055 "properties": {
4056 "street_address": { "type": "string" },
4057 "city": { "type": "string" },
4058 "state": { "type": "string" },
4059 "country": { "type": "string" }
4060 }
4061 }
4062 }
4063 }"#;
4064 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4065 let settings: CodeGenSettings = default_settings();
4066 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4067 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4068 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4069
4070use serde::{Deserialize, Serialize};
4071
4072#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4073pub struct Address {
4074 pub city: Option<String>,
4075 pub country: Option<String>,
4076 pub state: Option<String>,
4077 pub street_address: Option<String>,
4078}
4079
4080#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4081pub struct Root {
4082 pub address: Option<Address>,
4083 pub birthday: Option<String>,
4084 pub first_name: Option<String>,
4085 pub last_name: Option<String>,
4086}
4087
4088";
4089 assert_eq!(expected, actual);
4090 }
4091
4092 #[test]
4093 fn deeply_nested_schema_does_not_stack_overflow() {
4094 const DEPTH: usize = 150;
4095 let mut inner: JsonSchema = JsonSchema {
4096 type_: Some("object".to_string()),
4097 properties: {
4098 let mut m = std::collections::BTreeMap::new();
4099 m.insert(
4100 "value".to_string(),
4101 JsonSchema {
4102 type_: Some("string".to_string()),
4103 ..Default::default()
4104 },
4105 );
4106 m
4107 },
4108 title: Some("Leaf".to_string()),
4109 ..Default::default()
4110 };
4111 for i in (0..DEPTH).rev() {
4112 let mut wrap: JsonSchema = JsonSchema {
4113 type_: Some("object".to_string()),
4114 title: Some(format!("Level{i}")),
4115 ..Default::default()
4116 };
4117 wrap.properties.insert("child".to_string(), inner);
4118 inner = wrap;
4119 }
4120 let settings: CodeGenSettings = default_settings();
4121 let actual = generate_rust(&[inner], &settings);
4122 assert!(actual.is_ok(), concat!("deep schema must not ", "overflow"));
4123 let out = actual.unwrap();
4124 let output: String = String::from_utf8(out.per_schema[0].clone()).unwrap();
4125 assert!(
4126 output.contains(concat!("pub struct ", "Level0")),
4127 concat!("output must contain root ", "struct")
4128 );
4129 assert!(
4130 output.contains(concat!("pub struct ", "Leaf")),
4131 concat!("output must contain leaf ", "struct")
4132 );
4133 }
4134
4135 #[test]
4136 fn field_rename_when_key_differs_from_identifier() {
4137 let json = r#"{"type":"object","properties":{"foo-bar":{"type":"string"}}}"#;
4138 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4139 let settings: CodeGenSettings = default_settings();
4140 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4141 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4142 let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
4143
4144use serde::{Deserialize, Serialize};
4145
4146#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4147pub struct Root {
4148 #[serde(rename = "foo-bar")]
4149 pub foo_bar: Option<String>,
4150}
4151
4152"#;
4153 assert_eq!(expected, actual);
4154 }
4155
4156 #[test]
4157 fn generate_rust_one_schema_returns_one_buffer() {
4158 let json = r#"{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}"#;
4159 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4160 let settings: CodeGenSettings = default_settings();
4161 let output: super::GenerateRustOutput =
4162 generate_rust(std::slice::from_ref(&schema), &settings).unwrap();
4163 let expected: super::GenerateRustOutput =
4164 RustBackend.generate(&[schema], &settings).unwrap();
4165 assert_eq!(expected.per_schema, output.per_schema);
4166 assert_eq!(1, output.per_schema.len());
4167 }
4168
4169 #[test]
4170 fn property_key_first_uses_key_over_title_for_nested_struct() {
4171 let json = r#"{"type":"object","properties":{"address":{"type":"object","title":"FooBar","properties":{"city":{"type":"string"}}}}}"#;
4172 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4173 let settings: CodeGenSettings = CodeGenSettings::builder()
4174 .model_name_source(ModelNameSource::PropertyKeyFirst)
4175 .build();
4176 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4177 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4178 assert!(
4179 actual.contains(concat!("pub struct ", "Address")),
4180 "with PropertyKeyFirst nested struct should be named from key address -> Address; got: {actual}"
4181 );
4182 assert!(
4183 !actual.contains(concat!("struct ", "FooBar")),
4184 "with PropertyKeyFirst title FooBar should not be used for nested name; got: {actual}"
4185 );
4186 }
4187
4188 #[test]
4189 fn generate_rust_two_schemas_returns_two_buffers() {
4190 let json1 = r#"{"type":"object","properties":{"a":{"type":"string"}}}"#;
4191 let json2 = r#"{"type":"object","properties":{"b":{"type":"string"}}}"#;
4192 let s1: JsonSchema = serde_json::from_str(json1).unwrap();
4193 let s2: JsonSchema = serde_json::from_str(json2).unwrap();
4194 let settings: CodeGenSettings = default_settings();
4195 let output: super::GenerateRustOutput =
4196 generate_rust(&[s1.clone(), s2.clone()], &settings).unwrap();
4197 let expected: super::GenerateRustOutput =
4198 RustBackend.generate(&[s1, s2], &settings).unwrap();
4199 assert_eq!(expected.per_schema, output.per_schema);
4200 assert_eq!(2, output.per_schema.len());
4201 let out1 = String::from_utf8(output.per_schema[0].clone()).unwrap();
4202 let out2 = String::from_utf8(output.per_schema[1].clone()).unwrap();
4203 assert!(out1.contains("pub a: Option<String>") || out1.contains("pub a:"));
4204 assert!(out2.contains("pub b: Option<String>") || out2.contains("pub b:"));
4205 }
4206
4207 #[test]
4208 fn batch_error_includes_index() {
4209 let valid = r#"{"type":"object","properties":{"x":{"type":"string"}}}"#;
4210 let invalid: JsonSchema = JsonSchema::default();
4211 let s1: JsonSchema = serde_json::from_str(valid).unwrap();
4212 let settings: CodeGenSettings = default_settings();
4213 let actual = generate_rust(&[s1, invalid], &settings).unwrap_err();
4214 assert!(matches!(actual, CodeGenError::Batch { index: 1, .. }));
4215 }
4216
4217 #[test]
4218 fn dedupe_disabled_returns_no_shared() {
4219 let json = r#"{"type":"object","properties":{"a":{"type":"string"}}}"#;
4220 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4221 let settings: CodeGenSettings = CodeGenSettings::builder()
4222 .dedupe_mode(DedupeMode::Disabled)
4223 .build();
4224 let output: super::GenerateRustOutput =
4225 generate_rust(&[schema.clone(), schema], &settings).unwrap();
4226 let expected: Option<Vec<u8>> = None;
4227 let actual = output.shared.clone();
4228 assert_eq!(expected, actual);
4229 assert_eq!(2, output.per_schema.len());
4230 }
4231
4232 #[test]
4233 fn dedupe_disabled_two_schemas_same_shape_two_buffers_no_shared() {
4234 let json = r#"{"type":"object","properties":{"id":{"type":"string"}}}"#;
4235 let s1: JsonSchema = serde_json::from_str(json).unwrap();
4236 let s2: JsonSchema = serde_json::from_str(json).unwrap();
4237 let settings: CodeGenSettings = CodeGenSettings::builder()
4238 .dedupe_mode(DedupeMode::Disabled)
4239 .build();
4240 let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4241 assert_eq!(None, output.shared);
4242 assert_eq!(2, output.per_schema.len());
4243 let out0 = String::from_utf8(output.per_schema[0].clone()).unwrap();
4244 let out1 = String::from_utf8(output.per_schema[1].clone()).unwrap();
4245 let root_struct: &str = concat!("pub struct ", "Root");
4246 assert!(out0.contains(root_struct));
4247 assert!(out1.contains(root_struct));
4248 }
4249
4250 #[test]
4251 fn dedupe_full_two_identical_schemas_produces_shared() {
4252 let json = r#"{"type":"object","properties":{"id":{"type":"string"}}}"#;
4253 let s1: JsonSchema = serde_json::from_str(json).unwrap();
4254 let s2: JsonSchema = serde_json::from_str(json).unwrap();
4255 let settings: CodeGenSettings = CodeGenSettings::builder()
4256 .dedupe_mode(DedupeMode::Full)
4257 .build();
4258 let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4259 let expected_shared_some = true;
4260 let actual_shared_some = output.shared.is_some();
4261 assert_eq!(expected_shared_some, actual_shared_some);
4262 assert_eq!(2, output.per_schema.len());
4263 let shared_str = String::from_utf8(output.shared.unwrap()).unwrap();
4264 let root_struct: &str = concat!("pub struct ", "Root");
4265 assert!(shared_str.contains(root_struct));
4266 let per0 = String::from_utf8(output.per_schema[0].clone()).unwrap();
4267 let root_use: &str = concat!("pub use crate::", "Root");
4268 let root_only: &str = "Root";
4269 assert!(per0.contains(root_use) || per0.contains(root_only));
4270 }
4271
4272 #[test]
4273 fn dedupe_full_single_schema_no_shared() {
4274 let json = r#"{"type":"object","properties":{"a":{"type":"string"}}}"#;
4275 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4276 let settings: CodeGenSettings = CodeGenSettings::builder()
4277 .dedupe_mode(DedupeMode::Full)
4278 .build();
4279 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4280 let expected: Option<Vec<u8>> = None;
4281 let actual = output.shared.clone();
4282 assert_eq!(expected, actual);
4283 assert_eq!(1, output.per_schema.len());
4284 }
4285
4286 #[test]
4287 fn dedupe_full_two_different_schemas_no_shared() {
4288 let j1 = r#"{"type":"object","properties":{"a":{"type":"string"}}}"#;
4289 let j2 = r#"{"type":"object","properties":{"b":{"type":"string"}}}"#;
4290 let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4291 let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4292 let settings: CodeGenSettings = CodeGenSettings::builder()
4293 .dedupe_mode(DedupeMode::Full)
4294 .build();
4295 let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4296 assert_eq!(None, output.shared);
4297 assert_eq!(2, output.per_schema.len());
4298 }
4299
4300 #[test]
4301 fn dedupe_full_single_schema_two_identical_nested_objects_deduped() {
4302 let json = r#"{
4303 "type": "object",
4304 "properties": {
4305 "addr1": { "type": "object", "properties": { "street": { "type": "string" } } },
4306 "addr2": { "type": "object", "properties": { "street": { "type": "string" } } }
4307 }
4308 }"#;
4309 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4310 let settings: CodeGenSettings = CodeGenSettings::builder()
4311 .dedupe_mode(DedupeMode::Full)
4312 .build();
4313 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4314 let per_str = String::from_utf8(output.per_schema[0].clone()).unwrap();
4315 assert!(
4316 per_str.contains("addr1") && per_str.contains("addr2"),
4317 "per_schema should reference both fields; got: {per_str}"
4318 );
4319 let shared_count = output
4320 .shared
4321 .as_ref()
4322 .map_or(0, |b| b.windows(11).filter(|w| w == b"pub struct ").count());
4323 let per_count = per_str.matches("pub struct ").count();
4324 assert!(
4325 shared_count + per_count >= 1,
4326 "at least one struct (Root in per_schema, nested in shared when deduped)"
4327 );
4328 }
4329
4330 #[test]
4331 fn description_root_struct_single_line() {
4332 let json = r#"{"type":"object","properties":{"id":{"type":"string"}},"description":"A root type"}"#;
4333 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4334 let settings: CodeGenSettings = default_settings();
4335 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4336 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4337 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4338
4339use serde::{Deserialize, Serialize};
4340
4341/// A root type
4342#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4343pub struct Root {
4344 pub id: Option<String>,
4345}
4346
4347";
4348 assert_eq!(expected, actual);
4349 }
4350
4351 #[test]
4352 fn description_root_struct_multi_line() {
4353 let json = r#"{"type":"object","properties":{"x":{"type":"string"}},"description":"Line one\nLine two"}"#;
4354 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4355 let settings: CodeGenSettings = default_settings();
4356 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4357 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4358 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4359
4360use serde::{Deserialize, Serialize};
4361
4362/// Line one
4363/// Line two
4364#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4365pub struct Root {
4366 pub x: Option<String>,
4367}
4368
4369";
4370 assert_eq!(expected, actual);
4371 }
4372
4373 #[test]
4374 fn description_empty_or_whitespace_emits_no_doc() {
4375 let json_empty =
4376 r#"{"type":"object","properties":{"a":{"type":"string"}},"description":""}"#;
4377 let schema_empty: JsonSchema = serde_json::from_str(json_empty).unwrap();
4378 let settings: CodeGenSettings = default_settings();
4379 let output: super::GenerateRustOutput = generate_rust(&[schema_empty], &settings).unwrap();
4380 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4381 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4382
4383use serde::{Deserialize, Serialize};
4384
4385#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4386pub struct Root {
4387 pub a: Option<String>,
4388}
4389
4390";
4391 assert_eq!(expected, actual);
4392
4393 let json_ws =
4394 r#"{"type":"object","properties":{"a":{"type":"string"}},"description":" \n "}"#;
4395 let schema_ws: JsonSchema = serde_json::from_str(json_ws).unwrap();
4396 let output_ws: super::GenerateRustOutput = generate_rust(&[schema_ws], &settings).unwrap();
4397 let actual_ws = String::from_utf8(output_ws.per_schema[0].clone()).unwrap();
4398 assert_eq!(expected, actual_ws);
4399 }
4400
4401 #[test]
4402 fn description_nested_object_struct_doc() {
4403 let json = r#"{"type":"object","properties":{"nested":{"type":"object","description":"Inner type","properties":{"v":{"type":"string"}}}}}"#;
4404 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4405 let settings: CodeGenSettings = default_settings();
4406 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4407 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4408 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4409
4410use serde::{Deserialize, Serialize};
4411
4412/// Inner type
4413#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4414pub struct Nested {
4415 pub v: Option<String>,
4416}
4417
4418#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4419pub struct Root {
4420 /// Inner type
4421 pub nested: Option<Nested>,
4422}
4423
4424";
4425 assert_eq!(expected, actual);
4426 }
4427
4428 #[test]
4429 fn description_property_field_doc() {
4430 let json = r#"{"type":"object","properties":{"name":{"type":"string","description":"User full name"}}}"#;
4431 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4432 let settings: CodeGenSettings = default_settings();
4433 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4434 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4435 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4436
4437use serde::{Deserialize, Serialize};
4438
4439#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4440pub struct Root {
4441 /// User full name
4442 pub name: Option<String>,
4443}
4444
4445";
4446 assert_eq!(expected, actual);
4447 }
4448
4449 #[test]
4450 fn description_enum_property_emits_enum_doc() {
4451 let json = r#"{"type":"object","properties":{"status":{"enum":["open","closed"],"description":"Issue status"}},"required":["status"]}"#;
4452 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4453 let settings: CodeGenSettings = default_settings();
4454 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4455 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4456 let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
4457
4458use serde::{Deserialize, Serialize};
4459
4460/// Issue status
4461#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4462pub enum Status {
4463 #[serde(rename = "closed")]
4464 Closed,
4465 #[serde(rename = "open")]
4466 Open,
4467}
4468
4469#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4470pub struct Root {
4471 /// Issue status
4472 pub status: Status,
4473}
4474
4475"#;
4476 assert_eq!(expected, actual);
4477 }
4478
4479 #[test]
4480 fn dedupe_functional_same_shape_different_description_one_struct() {
4481 let j1 = r#"{"type":"object","properties":{"id":{"type":"string"}},"description":"First"}"#;
4482 let j2 =
4483 r#"{"type":"object","properties":{"id":{"type":"string"}},"description":"Second"}"#;
4484 let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4485 let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4486 let settings: CodeGenSettings = CodeGenSettings::builder()
4487 .dedupe_mode(DedupeMode::Functional)
4488 .build();
4489 let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4490 let expected_shared_some = true;
4491 let actual_shared_some = output.shared.is_some();
4492 assert_eq!(expected_shared_some, actual_shared_some);
4493 }
4494
4495 #[test]
4496 fn dedupe_full_same_shape_different_description_two_structs() {
4497 let j1 = r#"{"type":"object","properties":{"id":{"type":"string"}},"description":"First"}"#;
4498 let j2 =
4499 r#"{"type":"object","properties":{"id":{"type":"string"}},"description":"Second"}"#;
4500 let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4501 let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4502 let settings: CodeGenSettings = CodeGenSettings::builder()
4503 .dedupe_mode(DedupeMode::Full)
4504 .build();
4505 let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4506 let expected_shared_some = false;
4507 let actual_shared_some = output.shared.is_some();
4508 assert_eq!(expected_shared_some, actual_shared_some);
4509 assert_eq!(2, output.per_schema.len());
4510 }
4511
4512 #[test]
4513 fn dedupe_functional_same_shape_different_comment_one_struct() {
4514 let j1 = r#"{"type":"object","properties":{"id":{"type":"string"}},"$comment":"First"}"#;
4515 let j2 = r#"{"type":"object","properties":{"id":{"type":"string"}},"$comment":"Second"}"#;
4516 let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4517 let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4518 let settings: CodeGenSettings = CodeGenSettings::builder()
4519 .dedupe_mode(DedupeMode::Functional)
4520 .build();
4521 let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4522 let expected_shared_some = true;
4523 let actual_shared_some = output.shared.is_some();
4524 assert_eq!(expected_shared_some, actual_shared_some);
4525 }
4526
4527 #[test]
4528 fn dedupe_full_same_shape_different_comment_two_structs() {
4529 let j1 = r#"{"type":"object","properties":{"id":{"type":"string"}},"$comment":"First"}"#;
4530 let j2 = r#"{"type":"object","properties":{"id":{"type":"string"}},"$comment":"Second"}"#;
4531 let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4532 let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4533 let settings: CodeGenSettings = CodeGenSettings::builder()
4534 .dedupe_mode(DedupeMode::Full)
4535 .build();
4536 let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4537 let expected_shared_some = false;
4538 let actual_shared_some = output.shared.is_some();
4539 assert_eq!(expected_shared_some, actual_shared_some);
4540 assert_eq!(2, output.per_schema.len());
4541 }
4542
4543 #[test]
4544 fn examples_golden_with_examples_emits_doc() {
4545 let json: &str = r#"{"type":"object","properties":{"x":{"type":"string"}},"required":["x"],"examples":[{"x":"foo"}]}"#;
4546 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4547 let settings: CodeGenSettings = CodeGenSettings::default();
4548 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4549 let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
4550 let expected: String = "//! Generated by json-schema-rs. Do not edit manually.\n\nuse serde::{Deserialize, Serialize};\n\n/// \n/// # Examples\n/// \n/// {\"x\":\"foo\"}\n#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\npub struct Root {\n pub x: String,\n}\n\n".to_string();
4551 assert_eq!(expected, actual);
4552 }
4553
4554 #[test]
4555 fn examples_struct_doc_contains_examples() {
4556 let json: &str = r#"{"type":"object","properties":{"x":{"type":"string"}},"required":["x"],"examples":["a",1]}"#;
4557 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4558 let settings: CodeGenSettings = CodeGenSettings::default();
4559 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4560 let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
4561 let expected: String = "//! Generated by json-schema-rs. Do not edit manually.\n\nuse serde::{Deserialize, Serialize};\n\n/// \n/// # Examples\n/// \n/// \"a\"\n/// 1\n#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\npub struct Root {\n pub x: String,\n}\n\n".to_string();
4562 assert_eq!(expected, actual);
4563 }
4564
4565 #[test]
4566 fn examples_enum_doc_contains_examples() {
4567 let json: &str = r#"{"type":"object","properties":{"status":{"enum":["open"],"examples":["open"]}},"required":["status"]}"#;
4568 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4569 let settings: CodeGenSettings = CodeGenSettings::default();
4570 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4571 let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
4572 let expected: String = "//! Generated by json-schema-rs. Do not edit manually.\n\nuse serde::{Deserialize, Serialize};\n\n/// \n/// # Examples\n/// \n/// \"open\"\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\npub enum Status {\n #[serde(rename = \"open\")]\n Open,\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\npub struct Root {\n pub status: Status,\n}\n\n".to_string();
4573 assert_eq!(expected, actual);
4574 }
4575
4576 #[test]
4577 fn examples_empty_array_emits_no_doc() {
4578 let json: &str = r#"{"type":"object","properties":{"x":{"type":"string"}},"required":["x"],"examples":[]}"#;
4579 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4580 let settings: CodeGenSettings = CodeGenSettings::default();
4581 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4582 let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
4583 let expected: String =
4584 "//! Generated by json-schema-rs. Do not edit manually.\n\nuse serde::{Deserialize, Serialize};\n\n#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\npub struct Root {\n pub x: String,\n}\n\n".to_string();
4585 assert_eq!(expected, actual);
4586 }
4587
4588 #[test]
4589 fn dedupe_full_same_shape_different_examples_two_structs() {
4590 let j1 = r#"{"type":"object","properties":{"id":{"type":"string"}},"examples":[1]}"#;
4591 let j2 = r#"{"type":"object","properties":{"id":{"type":"string"}},"examples":[2]}"#;
4592 let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4593 let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4594 let settings: CodeGenSettings = CodeGenSettings::builder()
4595 .dedupe_mode(DedupeMode::Full)
4596 .build();
4597 let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4598 let expected: (bool, usize) = (false, 2);
4599 let actual: (bool, usize) = (output.shared.is_some(), output.per_schema.len());
4600 assert_eq!(expected, actual);
4601 }
4602
4603 #[test]
4604 fn dedupe_functional_same_shape_different_examples_one_struct() {
4605 let j1 = r#"{"type":"object","properties":{"id":{"type":"string"}},"examples":[1]}"#;
4606 let j2 = r#"{"type":"object","properties":{"id":{"type":"string"}},"examples":[2]}"#;
4607 let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4608 let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4609 let settings: CodeGenSettings = CodeGenSettings::builder()
4610 .dedupe_mode(DedupeMode::Functional)
4611 .build();
4612 let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4613 let expected_shared_some = true;
4614 let actual_shared_some = output.shared.is_some();
4615 assert_eq!(expected_shared_some, actual_shared_some);
4616 }
4617
4618 #[test]
4619 fn dedupe_full_same_shape_different_id_two_structs() {
4620 let j1 = r#"{"$id":"http://example.com/a","type":"object","properties":{"x":{"type":"string"}}}"#;
4621 let j2 = r#"{"$id":"http://example.com/b","type":"object","properties":{"x":{"type":"string"}}}"#;
4622 let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4623 let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4624 let settings: CodeGenSettings = CodeGenSettings::builder()
4625 .dedupe_mode(DedupeMode::Full)
4626 .build();
4627 let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4628 let expected_shared_some = false;
4629 let actual_shared_some = output.shared.is_some();
4630 let msg: &str = concat!(
4631 "Full dedupe: same shape different id yields two ",
4632 "structs"
4633 );
4634 assert_eq!(expected_shared_some, actual_shared_some, "{msg}");
4635 }
4636
4637 #[test]
4638 fn dedupe_functional_same_shape_different_id_one_struct() {
4639 let j1 = r#"{"$id":"http://example.com/a","type":"object","properties":{"x":{"type":"string"}}}"#;
4640 let j2 = r#"{"$id":"http://example.com/b","type":"object","properties":{"x":{"type":"string"}}}"#;
4641 let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4642 let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4643 let settings: CodeGenSettings = CodeGenSettings::builder()
4644 .dedupe_mode(DedupeMode::Functional)
4645 .build();
4646 let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4647 let expected_shared_some = true;
4648 let actual_shared_some = output.shared.is_some();
4649 let msg: &str = concat!(
4650 "Functional dedupe: same shape different id yields one shared ",
4651 "struct"
4652 );
4653 assert_eq!(expected_shared_some, actual_shared_some, "{msg}");
4654 }
4655
4656 #[test]
4657 fn dedupe_functional_same_as_full_when_no_description() {
4658 let json = r#"{"type":"object","properties":{"id":{"type":"string"}}}"#;
4659 let s1: JsonSchema = serde_json::from_str(json).unwrap();
4660 let s2: JsonSchema = serde_json::from_str(json).unwrap();
4661 let settings_full: CodeGenSettings = CodeGenSettings::builder()
4662 .dedupe_mode(DedupeMode::Full)
4663 .build();
4664 let settings_func: CodeGenSettings = CodeGenSettings::builder()
4665 .dedupe_mode(DedupeMode::Functional)
4666 .build();
4667 let output_full: super::GenerateRustOutput =
4668 generate_rust(&[s1.clone(), s2.clone()], &settings_full).unwrap();
4669 let output_func: super::GenerateRustOutput =
4670 generate_rust(&[s1, s2], &settings_func).unwrap();
4671 assert_eq!(output_full.shared.is_some(), output_func.shared.is_some());
4672 assert_eq!(output_full.per_schema.len(), output_func.per_schema.len());
4673 }
4674
4675 #[test]
4676 fn default_settings_use_full_dedupe() {
4677 let settings: CodeGenSettings = CodeGenSettings::builder().build();
4678 let expected = DedupeMode::Full;
4679 let actual = settings.dedupe_mode;
4680 assert_eq!(expected, actual);
4681 }
4682
4683 #[cfg(feature = "uuid")]
4684 #[test]
4685 fn uuid_required_property() {
4686 let json = r#"{"type":"object","properties":{"id":{"type":"string","format":"uuid"}},"required":["id"]}"#;
4687 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4688 let settings: CodeGenSettings = default_settings();
4689 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4690 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4691 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4692
4693use serde::{Deserialize, Serialize};
4694use uuid::Uuid;
4695
4696#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4697pub struct Root {
4698 pub id: Uuid,
4699}
4700
4701";
4702 assert_eq!(expected, actual);
4703 }
4704
4705 #[cfg(feature = "uuid")]
4706 #[test]
4707 fn uuid_optional_property() {
4708 let json = r#"{"type":"object","properties":{"id":{"type":"string","format":"uuid"}}}"#;
4709 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4710 let settings: CodeGenSettings = default_settings();
4711 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4712 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4713 let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4714
4715use serde::{Deserialize, Serialize};
4716use uuid::Uuid;
4717
4718#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4719pub struct Root {
4720 pub id: Option<Uuid>,
4721}
4722
4723";
4724 assert_eq!(expected, actual);
4725 }
4726
4727 #[cfg(feature = "uuid")]
4728 #[test]
4729 fn uuid_array_items() {
4730 let json = r#"{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["ids"]}"#;
4731 let schema: JsonSchema = serde_json::from_str(json).unwrap();
4732 let settings: CodeGenSettings = default_settings();
4733 let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4734 let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4735 let expected = "//! Generated by json-schema-rs. Do not edit manually.\n\nuse serde::{\"Deserialize\", \"Serialize\"};\nuse uuid::Uuid;\n\n#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\npub struct Root {\n pub ids: Vec<Uuid>,\n}\n\n";
4736 assert_eq!(expected, actual);
4737 }
4738}