1use std::collections::{HashMap, HashSet};
6
7use serde_json::Value;
8
9use crate::error_codes::SchemaErrorCode;
10use crate::json_source_locator::JsonSourceLocator;
11use crate::types::{
12 is_valid_type, JsonLocation, SchemaValidatorOptions, ValidationError, ValidationResult,
13 COMPOSITION_KEYWORDS, KNOWN_EXTENSIONS, VALIDATION_EXTENSION_KEYWORDS,
14};
15
16pub struct SchemaValidator {
28 options: SchemaValidatorOptions,
29 #[allow(dead_code)]
30 external_schemas: HashMap<String, Value>,
31}
32
33impl Default for SchemaValidator {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl SchemaValidator {
40 #[must_use]
42 pub fn new() -> Self {
43 Self::with_options(SchemaValidatorOptions::default())
44 }
45
46 #[must_use]
48 pub fn with_options(options: SchemaValidatorOptions) -> Self {
49 let mut external_schemas = HashMap::new();
50 for schema in &options.external_schemas {
51 if let Some(id) = schema.get("$id").and_then(Value::as_str) {
52 external_schemas.insert(id.to_string(), schema.clone());
53 }
54 }
55 Self {
56 options,
57 external_schemas,
58 }
59 }
60
61 pub fn set_extended(&mut self, _extended: bool) {
64 }
66
67 pub fn is_extended(&self) -> bool {
70 true }
72
73 pub fn set_warn_on_extension_keywords(&mut self, warn: bool) {
75 self.options.warn_on_unused_extension_keywords = warn;
76 }
77
78 pub fn is_warn_on_extension_keywords(&self) -> bool {
80 self.options.warn_on_unused_extension_keywords
81 }
82
83 #[must_use]
87 pub fn validate(&self, schema_json: &str) -> ValidationResult {
88 let mut result = ValidationResult::new();
89 let locator = JsonSourceLocator::new(schema_json);
90
91 match serde_json::from_str::<Value>(schema_json) {
92 Ok(schema) => {
93 self.validate_schema_internal(&schema, &schema, &locator, &mut result, "", true, &mut HashSet::new(), 0);
94 }
95 Err(e) => {
96 result.add_error(ValidationError::schema_error(
97 SchemaErrorCode::SchemaInvalidType,
98 format!("Failed to parse JSON: {}", e),
99 "",
100 JsonLocation::unknown(),
101 ));
102 }
103 }
104
105 result
106 }
107
108 #[must_use]
112 pub fn validate_value(&self, schema: &Value, schema_json: &str) -> ValidationResult {
113 let mut result = ValidationResult::new();
114 let locator = JsonSourceLocator::new(schema_json);
115 self.validate_schema_internal(schema, schema, &locator, &mut result, "", true, &mut HashSet::new(), 0);
116 result
117 }
118
119 fn validate_schema(
121 &self,
122 schema: &Value,
123 root_schema: &Value,
124 locator: &JsonSourceLocator,
125 result: &mut ValidationResult,
126 path: &str,
127 is_root: bool,
128 visited_refs: &mut HashSet<String>,
129 depth: usize,
130 ) {
131 self.validate_schema_internal(schema, root_schema, locator, result, path, is_root, visited_refs, depth);
133 }
134
135 fn validate_schema_internal(
137 &self,
138 schema: &Value,
139 root_schema: &Value,
140 locator: &JsonSourceLocator,
141 result: &mut ValidationResult,
142 path: &str,
143 is_root: bool,
144 visited_refs: &mut HashSet<String>,
145 depth: usize,
146 ) {
147 if depth > self.options.max_validation_depth {
148 return;
149 }
150
151 match schema {
153 Value::Null => {
154 result.add_error(ValidationError::schema_error(
155 SchemaErrorCode::SchemaNull,
156 "Schema cannot be null",
157 path,
158 locator.get_location(path),
159 ));
160 return;
161 }
162 Value::Bool(_) => {
163 return;
165 }
166 Value::Object(_obj) => {
167 }
169 _ => {
170 result.add_error(ValidationError::schema_error(
171 SchemaErrorCode::SchemaInvalidType,
172 "Schema must be an object or boolean",
173 path,
174 locator.get_location(path),
175 ));
176 return;
177 }
178 }
179
180 let obj = schema.as_object().unwrap();
181
182 let mut enabled_extensions = HashSet::new();
184 if let Some(root_obj) = root_schema.as_object() {
185 if let Some(Value::Array(arr)) = root_obj.get("$uses") {
186 for ext in arr {
187 if let Value::String(s) = ext {
188 enabled_extensions.insert(s.as_str());
189 }
190 }
191 }
192 }
193
194 if is_root {
196 self.validate_root_schema(obj, locator, result, path);
197 }
198
199 if let Some(ref_val) = obj.get("$ref") {
201 self.validate_ref(ref_val, schema, root_schema, locator, result, path, visited_refs, depth);
202 }
203
204 if let Some(type_val) = obj.get("type") {
206 self.validate_type(type_val, obj, root_schema, locator, result, path, &enabled_extensions, visited_refs, depth);
207 }
208
209 if let Some(defs) = obj.get("definitions") {
211 self.validate_definitions(defs, root_schema, locator, result, path, visited_refs, depth);
212 }
213
214 if let Some(enum_val) = obj.get("enum") {
216 self.validate_enum(enum_val, locator, result, path);
217 }
218
219 self.validate_composition(obj, root_schema, locator, result, path, &enabled_extensions, visited_refs, depth);
221
222 if self.options.warn_on_unused_extension_keywords {
224 self.check_extension_keywords(obj, locator, result, path, &enabled_extensions);
225 }
226 }
227
228 fn validate_root_schema(
230 &self,
231 obj: &serde_json::Map<String, Value>,
232 locator: &JsonSourceLocator,
233 result: &mut ValidationResult,
234 path: &str,
235 ) {
236 if !obj.contains_key("$id") {
238 result.add_error(ValidationError::schema_error(
239 SchemaErrorCode::SchemaRootMissingId,
240 "Root schema must have $id",
241 path,
242 locator.get_location(path),
243 ));
244 } else if let Some(id) = obj.get("$id") {
245 if !id.is_string() {
246 result.add_error(ValidationError::schema_error(
247 SchemaErrorCode::SchemaRootMissingId,
248 "$id must be a string",
249 &format!("{}/$id", path),
250 locator.get_location(&format!("{}/$id", path)),
251 ));
252 }
253 }
254
255 if obj.contains_key("type") && !obj.contains_key("name") {
257 result.add_error(ValidationError::schema_error(
258 SchemaErrorCode::SchemaRootMissingName,
259 "Root schema with type must have name",
260 path,
261 locator.get_location(path),
262 ));
263 }
264
265 let has_type = obj.contains_key("type");
267 let has_root = obj.contains_key("$root");
268 let has_definitions = obj.contains_key("definitions");
269 let has_composition = obj.keys().any(|k|
270 ["allOf", "anyOf", "oneOf", "not", "if"].contains(&k.as_str())
271 );
272
273 if !has_type && !has_root && !has_composition {
274 let has_only_meta = obj.keys().all(|k| {
276 k.starts_with('$') || k == "definitions" || k == "name" || k == "description"
277 });
278
279 if !has_only_meta || !has_definitions {
280 result.add_error(ValidationError::schema_error(
281 SchemaErrorCode::SchemaRootMissingType,
282 "Schema must have a 'type' property or '$root' reference",
283 path,
284 locator.get_location(path),
285 ));
286 }
287 }
288
289 if let Some(uses) = obj.get("$uses") {
291 self.validate_uses(uses, locator, result, path);
292 }
293
294 if let Some(offers) = obj.get("$offers") {
296 self.validate_offers(offers, locator, result, path);
297 }
298 }
299
300 fn validate_uses(
302 &self,
303 uses: &Value,
304 locator: &JsonSourceLocator,
305 result: &mut ValidationResult,
306 path: &str,
307 ) {
308 let uses_path = format!("{}/$uses", path);
309 match uses {
310 Value::Array(arr) => {
311 for (i, ext) in arr.iter().enumerate() {
312 if let Value::String(s) = ext {
313 if !KNOWN_EXTENSIONS.contains(&s.as_str()) {
314 result.add_error(ValidationError::schema_warning(
315 SchemaErrorCode::SchemaUsesInvalidExtension,
316 format!("Unknown extension: {}", s),
317 &format!("{}/{}", uses_path, i),
318 locator.get_location(&format!("{}/{}", uses_path, i)),
319 ));
320 }
321 } else {
322 result.add_error(ValidationError::schema_error(
323 SchemaErrorCode::SchemaUsesInvalidExtension,
324 "Extension name must be a string",
325 &format!("{}/{}", uses_path, i),
326 locator.get_location(&format!("{}/{}", uses_path, i)),
327 ));
328 }
329 }
330 }
331 _ => {
332 result.add_error(ValidationError::schema_error(
333 SchemaErrorCode::SchemaUsesNotArray,
334 "$uses must be an array",
335 &uses_path,
336 locator.get_location(&uses_path),
337 ));
338 }
339 }
340 }
341
342 fn validate_offers(
344 &self,
345 offers: &Value,
346 locator: &JsonSourceLocator,
347 result: &mut ValidationResult,
348 path: &str,
349 ) {
350 let offers_path = format!("{}/$offers", path);
351 match offers {
352 Value::Array(arr) => {
353 for (i, ext) in arr.iter().enumerate() {
354 if !ext.is_string() {
355 result.add_error(ValidationError::schema_error(
356 SchemaErrorCode::SchemaOffersInvalidExtension,
357 "Extension name must be a string",
358 &format!("{}/{}", offers_path, i),
359 locator.get_location(&format!("{}/{}", offers_path, i)),
360 ));
361 }
362 }
363 }
364 _ => {
365 result.add_error(ValidationError::schema_error(
366 SchemaErrorCode::SchemaOffersNotArray,
367 "$offers must be an array",
368 &offers_path,
369 locator.get_location(&offers_path),
370 ));
371 }
372 }
373 }
374
375 fn validate_ref(
377 &self,
378 ref_val: &Value,
379 _schema: &Value,
380 root_schema: &Value,
381 locator: &JsonSourceLocator,
382 result: &mut ValidationResult,
383 path: &str,
384 visited_refs: &mut HashSet<String>,
385 _depth: usize,
386 ) {
387 let ref_path = format!("{}/$ref", path);
388
389 match ref_val {
390 Value::String(ref_str) => {
391 if visited_refs.contains(ref_str) {
393 result.add_error(ValidationError::schema_error(
394 SchemaErrorCode::SchemaRefCircular,
395 format!("Circular reference detected: {}", ref_str),
396 &ref_path,
397 locator.get_location(&ref_path),
398 ));
399 return;
400 }
401
402 if ref_str.starts_with("#/definitions/") {
404 if let Some(resolved) = self.resolve_ref(ref_str, root_schema) {
406 if let Value::Object(def_obj) = resolved {
408 let keys: Vec<&String> = def_obj.keys().collect();
409 let is_bare_ref = keys.len() == 1 && keys[0] == "$ref";
410 let is_type_ref_only = keys.len() == 1 && keys[0] == "type" && {
411 if let Some(Value::Object(type_obj)) = def_obj.get("type") {
412 type_obj.len() == 1 && type_obj.contains_key("$ref")
413 } else {
414 false
415 }
416 };
417
418 if is_bare_ref || is_type_ref_only {
419 let inner_ref = if is_bare_ref {
421 def_obj.get("$ref").and_then(|v| v.as_str())
422 } else if is_type_ref_only {
423 def_obj.get("type")
424 .and_then(|t| t.as_object())
425 .and_then(|o| o.get("$ref"))
426 .and_then(|v| v.as_str())
427 } else {
428 None
429 };
430
431 if inner_ref == Some(ref_str) {
432 result.add_error(ValidationError::schema_error(
433 SchemaErrorCode::SchemaRefCircular,
434 format!("Direct circular reference: {}", ref_str),
435 &ref_path,
436 locator.get_location(&ref_path),
437 ));
438 }
439 }
440 }
441 } else {
442 result.add_error(ValidationError::schema_error(
443 SchemaErrorCode::SchemaRefNotFound,
444 format!("Reference not found: {}", ref_str),
445 &ref_path,
446 locator.get_location(&ref_path),
447 ));
448 }
449 }
450 }
452 _ => {
453 result.add_error(ValidationError::schema_error(
454 SchemaErrorCode::SchemaRefNotString,
455 "$ref must be a string",
456 &ref_path,
457 locator.get_location(&ref_path),
458 ));
459 }
460 }
461 }
462
463 fn resolve_ref<'a>(&self, ref_str: &str, root_schema: &'a Value) -> Option<&'a Value> {
465 if !ref_str.starts_with("#/") {
466 return None;
467 }
468
469 let path_parts: Vec<&str> = ref_str[2..].split('/').collect();
470 let mut current = root_schema;
471
472 for part in path_parts {
473 let unescaped = part.replace("~1", "/").replace("~0", "~");
475 current = current.get(&unescaped)?;
476 }
477
478 Some(current)
479 }
480
481 fn validate_type(
483 &self,
484 type_val: &Value,
485 obj: &serde_json::Map<String, Value>,
486 root_schema: &Value,
487 locator: &JsonSourceLocator,
488 result: &mut ValidationResult,
489 path: &str,
490 enabled_extensions: &HashSet<&str>,
491 visited_refs: &mut HashSet<String>,
492 depth: usize,
493 ) {
494 let type_path = format!("{}/type", path);
495
496 match type_val {
497 Value::String(type_name) => {
498 self.validate_single_type(type_name, obj, root_schema, locator, result, path, enabled_extensions, depth);
499 }
500 Value::Array(types) => {
501 if types.is_empty() {
503 result.add_error(ValidationError::schema_error(
504 SchemaErrorCode::SchemaTypeArrayEmpty,
505 "Union type array cannot be empty",
506 &type_path,
507 locator.get_location(&type_path),
508 ));
509 return;
510 }
511 for (i, t) in types.iter().enumerate() {
512 let elem_path = format!("{}/{}", type_path, i);
513 match t {
514 Value::String(s) => {
515 if !is_valid_type(s) {
516 result.add_error(ValidationError::schema_error(
517 SchemaErrorCode::SchemaTypeInvalid,
518 format!("Unknown type in union: '{}'", s),
519 &elem_path,
520 locator.get_location(&elem_path),
521 ));
522 }
523 }
524 Value::Object(ref_obj) => {
525 if let Some(ref_val) = ref_obj.get("$ref") {
526 if let Value::String(ref_str) = ref_val {
527 self.validate_type_ref(ref_str, root_schema, locator, result, &elem_path, visited_refs);
529 } else {
530 result.add_error(ValidationError::schema_error(
531 SchemaErrorCode::SchemaRefNotString,
532 "$ref must be a string",
533 &format!("{}/$ref", elem_path),
534 locator.get_location(&format!("{}/$ref", elem_path)),
535 ));
536 }
537 } else {
538 result.add_error(ValidationError::schema_error(
539 SchemaErrorCode::SchemaTypeObjectMissingRef,
540 "Union type object must have $ref",
541 &elem_path,
542 locator.get_location(&elem_path),
543 ));
544 }
545 }
546 _ => {
547 result.add_error(ValidationError::schema_error(
548 SchemaErrorCode::SchemaKeywordInvalidType,
549 "Union type elements must be strings or $ref objects",
550 &elem_path,
551 locator.get_location(&elem_path),
552 ));
553 }
554 }
555 }
556 }
557 Value::Object(ref_obj) => {
558 if let Some(ref_val) = ref_obj.get("$ref") {
560 if let Value::String(ref_str) = ref_val {
561 self.validate_type_ref(ref_str, root_schema, locator, result, path, visited_refs);
563 } else {
564 result.add_error(ValidationError::schema_error(
565 SchemaErrorCode::SchemaRefNotString,
566 "$ref must be a string",
567 &format!("{}/$ref", type_path),
568 locator.get_location(&format!("{}/$ref", type_path)),
569 ));
570 }
571 } else {
572 result.add_error(ValidationError::schema_error(
573 SchemaErrorCode::SchemaTypeObjectMissingRef,
574 "type object must have $ref",
575 &type_path,
576 locator.get_location(&type_path),
577 ));
578 }
579 }
580 _ => {
581 result.add_error(ValidationError::schema_error(
582 SchemaErrorCode::SchemaKeywordInvalidType,
583 "type must be a string, array, or object with $ref",
584 &type_path,
585 locator.get_location(&type_path),
586 ));
587 }
588 }
589 }
590
591 fn validate_type_ref(
593 &self,
594 ref_str: &str,
595 root_schema: &Value,
596 locator: &JsonSourceLocator,
597 result: &mut ValidationResult,
598 path: &str,
599 visited_refs: &mut HashSet<String>,
600 ) {
601 let ref_path = format!("{}/type/$ref", path);
602
603 if ref_str.starts_with("#/definitions/") {
604 if visited_refs.contains(ref_str) {
606 if let Some(resolved) = self.resolve_ref(ref_str, root_schema) {
608 if let Value::Object(def_obj) = resolved {
609 let keys: Vec<&String> = def_obj.keys().collect();
610 let is_type_ref_only = keys.len() == 1 && keys[0] == "type" && {
611 if let Some(Value::Object(type_obj)) = def_obj.get("type") {
612 type_obj.len() == 1 && type_obj.contains_key("$ref")
613 } else {
614 false
615 }
616 };
617
618 if is_type_ref_only {
619 result.add_error(ValidationError::schema_error(
620 SchemaErrorCode::SchemaRefCircular,
621 format!("Direct circular reference: {}", ref_str),
622 &ref_path,
623 locator.get_location(&ref_path),
624 ));
625 }
626 }
627 }
628 return;
629 }
630
631 visited_refs.insert(ref_str.to_string());
633
634 if let Some(resolved) = self.resolve_ref(ref_str, root_schema) {
636 if let Value::Object(def_obj) = resolved {
638 if let Some(type_val) = def_obj.get("type") {
639 if let Value::Object(type_obj) = type_val {
640 if let Some(Value::String(inner_ref)) = type_obj.get("$ref") {
641 self.validate_type_ref(inner_ref, root_schema, locator, result, path, visited_refs);
642 }
643 }
644 }
645 }
646 } else {
647 result.add_error(ValidationError::schema_error(
648 SchemaErrorCode::SchemaRefNotFound,
649 format!("Reference not found: {}", ref_str),
650 &ref_path,
651 locator.get_location(&ref_path),
652 ));
653 }
654
655 visited_refs.remove(ref_str);
656 }
657 }
658
659 fn validate_single_type(
661 &self,
662 type_name: &str,
663 obj: &serde_json::Map<String, Value>,
664 root_schema: &Value,
665 locator: &JsonSourceLocator,
666 result: &mut ValidationResult,
667 path: &str,
668 enabled_extensions: &HashSet<&str>,
669 depth: usize,
670 ) {
671 let type_path = format!("{}/type", path);
672
673 if !is_valid_type(type_name) {
675 result.add_error(ValidationError::schema_error(
676 SchemaErrorCode::SchemaTypeInvalid,
677 format!("Invalid type: {}", type_name),
678 &type_path,
679 locator.get_location(&type_path),
680 ));
681 return;
682 }
683
684 match type_name {
686 "object" => self.validate_object_type(obj, root_schema, locator, result, path, enabled_extensions, depth),
687 "array" | "set" => self.validate_array_type(obj, root_schema, locator, result, path, type_name),
688 "map" => self.validate_map_type(obj, root_schema, locator, result, path),
689 "tuple" => self.validate_tuple_type(obj, root_schema, locator, result, path),
690 "choice" => self.validate_choice_type(obj, root_schema, locator, result, path),
691 _ => {
692 self.validate_primitive_constraints(type_name, obj, locator, result, path);
694 }
695 }
696 }
697
698 fn validate_primitive_constraints(
700 &self,
701 type_name: &str,
702 obj: &serde_json::Map<String, Value>,
703 locator: &JsonSourceLocator,
704 result: &mut ValidationResult,
705 path: &str,
706 ) {
707 use crate::types::is_numeric_type;
708
709 let is_numeric = is_numeric_type(type_name);
711 let is_string = type_name == "string";
712
713 if obj.contains_key("minimum") && !is_numeric {
715 result.add_error(ValidationError::schema_error(
716 SchemaErrorCode::SchemaConstraintTypeMismatch,
717 format!("minimum constraint cannot be used with type '{}'", type_name),
718 &format!("{}/minimum", path),
719 locator.get_location(&format!("{}/minimum", path)),
720 ));
721 }
722 if obj.contains_key("maximum") && !is_numeric {
723 result.add_error(ValidationError::schema_error(
724 SchemaErrorCode::SchemaConstraintTypeMismatch,
725 format!("maximum constraint cannot be used with type '{}'", type_name),
726 &format!("{}/maximum", path),
727 locator.get_location(&format!("{}/maximum", path)),
728 ));
729 }
730
731 if obj.contains_key("minLength") && !is_string {
733 result.add_error(ValidationError::schema_error(
734 SchemaErrorCode::SchemaConstraintTypeMismatch,
735 format!("minLength constraint cannot be used with type '{}'", type_name),
736 &format!("{}/minLength", path),
737 locator.get_location(&format!("{}/minLength", path)),
738 ));
739 }
740 if obj.contains_key("maxLength") && !is_string {
741 result.add_error(ValidationError::schema_error(
742 SchemaErrorCode::SchemaConstraintTypeMismatch,
743 format!("maxLength constraint cannot be used with type '{}'", type_name),
744 &format!("{}/maxLength", path),
745 locator.get_location(&format!("{}/maxLength", path)),
746 ));
747 }
748
749 if is_numeric {
751 self.validate_numeric_constraints(obj, locator, result, path);
752 }
753
754 if is_string {
756 self.validate_string_constraints(obj, locator, result, path);
757 }
758
759 if let Some(multiple_of) = obj.get("multipleOf") {
761 if !is_numeric {
762 result.add_error(ValidationError::schema_error(
763 SchemaErrorCode::SchemaConstraintTypeMismatch,
764 format!("multipleOf constraint cannot be used with type '{}'", type_name),
765 &format!("{}/multipleOf", path),
766 locator.get_location(&format!("{}/multipleOf", path)),
767 ));
768 } else if let Some(n) = multiple_of.as_f64() {
769 if n <= 0.0 {
770 result.add_error(ValidationError::schema_error(
771 SchemaErrorCode::SchemaMultipleOfInvalid,
772 "multipleOf must be greater than 0",
773 &format!("{}/multipleOf", path),
774 locator.get_location(&format!("{}/multipleOf", path)),
775 ));
776 }
777 }
778 }
779
780 if let Some(Value::String(pattern)) = obj.get("pattern") {
782 if regex::Regex::new(pattern).is_err() {
783 result.add_error(ValidationError::schema_error(
784 SchemaErrorCode::SchemaPatternInvalid,
785 format!("Invalid regular expression pattern: {}", pattern),
786 &format!("{}/pattern", path),
787 locator.get_location(&format!("{}/pattern", path)),
788 ));
789 }
790 }
791 }
792
793 fn validate_numeric_constraints(
795 &self,
796 obj: &serde_json::Map<String, Value>,
797 locator: &JsonSourceLocator,
798 result: &mut ValidationResult,
799 path: &str,
800 ) {
801 let minimum = obj.get("minimum").and_then(Value::as_f64);
802 let maximum = obj.get("maximum").and_then(Value::as_f64);
803
804 if let (Some(min), Some(max)) = (minimum, maximum) {
805 if min > max {
806 result.add_error(ValidationError::schema_error(
807 SchemaErrorCode::SchemaMinimumExceedsMaximum,
808 format!("minimum ({}) exceeds maximum ({})", min, max),
809 &format!("{}/minimum", path),
810 locator.get_location(&format!("{}/minimum", path)),
811 ));
812 }
813 }
814 }
815
816 fn validate_string_constraints(
818 &self,
819 obj: &serde_json::Map<String, Value>,
820 locator: &JsonSourceLocator,
821 result: &mut ValidationResult,
822 path: &str,
823 ) {
824 let min_length = obj.get("minLength").and_then(Value::as_i64);
825 let max_length = obj.get("maxLength").and_then(Value::as_i64);
826
827 if let Some(min) = min_length {
828 if min < 0 {
829 result.add_error(ValidationError::schema_error(
830 SchemaErrorCode::SchemaMinLengthNegative,
831 "minLength cannot be negative",
832 &format!("{}/minLength", path),
833 locator.get_location(&format!("{}/minLength", path)),
834 ));
835 }
836 }
837
838 if let Some(max) = max_length {
839 if max < 0 {
840 result.add_error(ValidationError::schema_error(
841 SchemaErrorCode::SchemaMaxLengthNegative,
842 "maxLength cannot be negative",
843 &format!("{}/maxLength", path),
844 locator.get_location(&format!("{}/maxLength", path)),
845 ));
846 }
847 }
848
849 if let (Some(min), Some(max)) = (min_length, max_length) {
850 if min > max {
851 result.add_error(ValidationError::schema_error(
852 SchemaErrorCode::SchemaMinLengthExceedsMaxLength,
853 format!("minLength ({}) exceeds maxLength ({})", min, max),
854 &format!("{}/minLength", path),
855 locator.get_location(&format!("{}/minLength", path)),
856 ));
857 }
858 }
859 }
860
861 fn validate_object_type(
863 &self,
864 obj: &serde_json::Map<String, Value>,
865 root_schema: &Value,
866 locator: &JsonSourceLocator,
867 result: &mut ValidationResult,
868 path: &str,
869 _enabled_extensions: &HashSet<&str>,
870 depth: usize,
871 ) {
872 if let Some(props) = obj.get("properties") {
874 let props_path = format!("{}/properties", path);
875 match props {
876 Value::Object(props_obj) => {
877 for (prop_name, prop_schema) in props_obj {
878 let prop_path = format!("{}/{}", props_path, prop_name);
879 self.validate_schema(
880 prop_schema,
881 root_schema,
882 locator,
883 result,
884 &prop_path,
885 false,
886 &mut HashSet::new(),
887 depth + 1,
888 );
889 }
890 }
891 _ => {
892 result.add_error(ValidationError::schema_error(
893 SchemaErrorCode::SchemaPropertiesMustBeObject,
894 "properties must be an object",
895 &props_path,
896 locator.get_location(&props_path),
897 ));
898 }
899 }
900 }
901
902 if let Some(required) = obj.get("required") {
904 self.validate_required(required, obj.get("properties"), locator, result, path);
905 }
906
907 if let Some(additional) = obj.get("additionalProperties") {
909 let add_path = format!("{}/additionalProperties", path);
910 match additional {
911 Value::Bool(_) => {}
912 Value::Object(_) => {
913 self.validate_schema(
914 additional,
915 root_schema,
916 locator,
917 result,
918 &add_path,
919 false,
920 &mut HashSet::new(),
921 depth + 1,
922 );
923 }
924 _ => {
925 result.add_error(ValidationError::schema_error(
926 SchemaErrorCode::SchemaAdditionalPropertiesInvalid,
927 "additionalProperties must be a boolean or schema object",
928 &add_path,
929 locator.get_location(&add_path),
930 ));
931 }
932 }
933 }
934 }
935
936 fn validate_required(
938 &self,
939 required: &Value,
940 properties: Option<&Value>,
941 locator: &JsonSourceLocator,
942 result: &mut ValidationResult,
943 path: &str,
944 ) {
945 let required_path = format!("{}/required", path);
946
947 match required {
948 Value::Array(arr) => {
949 let mut seen = HashSet::new();
950 for (i, item) in arr.iter().enumerate() {
951 match item {
952 Value::String(s) => {
953 if !seen.insert(s.clone()) {
955 result.add_error(ValidationError::schema_warning(
956 SchemaErrorCode::SchemaRequiredPropertyNotDefined,
957 format!("Duplicate required property: {}", s),
958 &format!("{}/{}", required_path, i),
959 locator.get_location(&format!("{}/{}", required_path, i)),
960 ));
961 }
962
963 if let Some(Value::Object(props)) = properties {
965 if !props.contains_key(s) {
966 result.add_error(ValidationError::schema_error(
967 SchemaErrorCode::SchemaRequiredPropertyNotDefined,
968 format!("Required property not defined: {}", s),
969 &format!("{}/{}", required_path, i),
970 locator.get_location(&format!("{}/{}", required_path, i)),
971 ));
972 }
973 }
974 }
975 _ => {
976 result.add_error(ValidationError::schema_error(
977 SchemaErrorCode::SchemaRequiredItemMustBeString,
978 "Required item must be a string",
979 &format!("{}/{}", required_path, i),
980 locator.get_location(&format!("{}/{}", required_path, i)),
981 ));
982 }
983 }
984 }
985 }
986 _ => {
987 result.add_error(ValidationError::schema_error(
988 SchemaErrorCode::SchemaRequiredMustBeArray,
989 "required must be an array",
990 &required_path,
991 locator.get_location(&required_path),
992 ));
993 }
994 }
995 }
996
997 fn validate_array_type(
999 &self,
1000 obj: &serde_json::Map<String, Value>,
1001 root_schema: &Value,
1002 locator: &JsonSourceLocator,
1003 result: &mut ValidationResult,
1004 path: &str,
1005 type_name: &str,
1006 ) {
1007 if !obj.contains_key("items") {
1009 result.add_error(ValidationError::schema_error(
1010 SchemaErrorCode::SchemaArrayMissingItems,
1011 format!("{} type requires items keyword", type_name),
1012 path,
1013 locator.get_location(path),
1014 ));
1015 } else if let Some(items) = obj.get("items") {
1016 let items_path = format!("{}/items", path);
1017 self.validate_schema(items, root_schema, locator, result, &items_path, false, &mut HashSet::new(), 0);
1018 }
1019
1020 let min_items = obj.get("minItems").and_then(Value::as_i64);
1022 let max_items = obj.get("maxItems").and_then(Value::as_i64);
1023
1024 if let Some(min) = min_items {
1025 if min < 0 {
1026 result.add_error(ValidationError::schema_error(
1027 SchemaErrorCode::SchemaMinItemsNegative,
1028 "minItems cannot be negative",
1029 &format!("{}/minItems", path),
1030 locator.get_location(&format!("{}/minItems", path)),
1031 ));
1032 }
1033 }
1034
1035 if let (Some(min), Some(max)) = (min_items, max_items) {
1036 if min > max {
1037 result.add_error(ValidationError::schema_error(
1038 SchemaErrorCode::SchemaMinItemsExceedsMaxItems,
1039 format!("minItems ({}) exceeds maxItems ({})", min, max),
1040 &format!("{}/minItems", path),
1041 locator.get_location(&format!("{}/minItems", path)),
1042 ));
1043 }
1044 }
1045 }
1046
1047 fn validate_map_type(
1049 &self,
1050 obj: &serde_json::Map<String, Value>,
1051 root_schema: &Value,
1052 locator: &JsonSourceLocator,
1053 result: &mut ValidationResult,
1054 path: &str,
1055 ) {
1056 if !obj.contains_key("values") {
1058 result.add_error(ValidationError::schema_error(
1059 SchemaErrorCode::SchemaMapMissingValues,
1060 "map type requires values keyword",
1061 path,
1062 locator.get_location(path),
1063 ));
1064 } else if let Some(values) = obj.get("values") {
1065 let values_path = format!("{}/values", path);
1066 self.validate_schema(values, root_schema, locator, result, &values_path, false, &mut HashSet::new(), 0);
1067 }
1068 }
1069
1070 fn validate_tuple_type(
1072 &self,
1073 obj: &serde_json::Map<String, Value>,
1074 _root_schema: &Value,
1075 locator: &JsonSourceLocator,
1076 result: &mut ValidationResult,
1077 path: &str,
1078 ) {
1079 let has_properties = obj.contains_key("properties");
1081 let has_tuple = obj.contains_key("tuple");
1082
1083 if !has_properties || !has_tuple {
1084 result.add_error(ValidationError::schema_error(
1085 SchemaErrorCode::SchemaTupleMissingDefinition,
1086 "tuple type requires both properties and tuple keywords",
1087 path,
1088 locator.get_location(path),
1089 ));
1090 return;
1091 }
1092
1093 if let Some(tuple) = obj.get("tuple") {
1095 let tuple_path = format!("{}/tuple", path);
1096 match tuple {
1097 Value::Array(arr) => {
1098 let properties = obj.get("properties").and_then(Value::as_object);
1099
1100 for (i, item) in arr.iter().enumerate() {
1101 match item {
1102 Value::String(s) => {
1103 if let Some(props) = properties {
1105 if !props.contains_key(s) {
1106 result.add_error(ValidationError::schema_error(
1107 SchemaErrorCode::SchemaTuplePropertyNotDefined,
1108 format!("Tuple element references undefined property: {}", s),
1109 &format!("{}/{}", tuple_path, i),
1110 locator.get_location(&format!("{}/{}", tuple_path, i)),
1111 ));
1112 }
1113 }
1114 }
1115 _ => {
1116 result.add_error(ValidationError::schema_error(
1117 SchemaErrorCode::SchemaTupleInvalidFormat,
1118 "Tuple element must be a string (property name)",
1119 &format!("{}/{}", tuple_path, i),
1120 locator.get_location(&format!("{}/{}", tuple_path, i)),
1121 ));
1122 }
1123 }
1124 }
1125 }
1126 _ => {
1127 result.add_error(ValidationError::schema_error(
1128 SchemaErrorCode::SchemaTupleInvalidFormat,
1129 "tuple must be an array",
1130 &tuple_path,
1131 locator.get_location(&tuple_path),
1132 ));
1133 }
1134 }
1135 }
1136 }
1137
1138 fn validate_choice_type(
1140 &self,
1141 obj: &serde_json::Map<String, Value>,
1142 root_schema: &Value,
1143 locator: &JsonSourceLocator,
1144 result: &mut ValidationResult,
1145 path: &str,
1146 ) {
1147 if !obj.contains_key("choices") {
1149 result.add_error(ValidationError::schema_error(
1150 SchemaErrorCode::SchemaChoiceMissingChoices,
1151 "choice type requires choices keyword",
1152 path,
1153 locator.get_location(path),
1154 ));
1155 return;
1156 }
1157
1158 if let Some(choices) = obj.get("choices") {
1160 let choices_path = format!("{}/choices", path);
1161 match choices {
1162 Value::Object(choices_obj) => {
1163 for (choice_name, choice_schema) in choices_obj {
1164 let choice_path = format!("{}/{}", choices_path, choice_name);
1165 self.validate_schema(
1166 choice_schema,
1167 root_schema,
1168 locator,
1169 result,
1170 &choice_path,
1171 false,
1172 &mut HashSet::new(),
1173 0,
1174 );
1175 }
1176 }
1177 _ => {
1178 result.add_error(ValidationError::schema_error(
1179 SchemaErrorCode::SchemaChoicesNotObject,
1180 "choices must be an object",
1181 &choices_path,
1182 locator.get_location(&choices_path),
1183 ));
1184 }
1185 }
1186 }
1187
1188 if let Some(selector) = obj.get("selector") {
1190 let selector_path = format!("{}/selector", path);
1191 if !selector.is_string() {
1192 result.add_error(ValidationError::schema_error(
1193 SchemaErrorCode::SchemaSelectorNotString,
1194 "selector must be a string",
1195 &selector_path,
1196 locator.get_location(&selector_path),
1197 ));
1198 }
1199 }
1200 }
1201
1202 fn validate_definitions(
1204 &self,
1205 defs: &Value,
1206 root_schema: &Value,
1207 locator: &JsonSourceLocator,
1208 result: &mut ValidationResult,
1209 path: &str,
1210 visited_refs: &mut HashSet<String>,
1211 depth: usize,
1212 ) {
1213 let defs_path = format!("{}/definitions", path);
1214
1215 match defs {
1216 Value::Object(defs_obj) => {
1217 for (def_name, def_schema) in defs_obj {
1218 let def_path = format!("{}/{}", defs_path, def_name);
1219 self.validate_definition_or_namespace(def_schema, root_schema, locator, result, &def_path, visited_refs, depth);
1220 }
1221 }
1222 _ => {
1223 result.add_error(ValidationError::schema_error(
1224 SchemaErrorCode::SchemaDefinitionsMustBeObject,
1225 "definitions must be an object",
1226 &defs_path,
1227 locator.get_location(&defs_path),
1228 ));
1229 }
1230 }
1231 }
1232
1233 fn validate_definition_or_namespace(
1235 &self,
1236 def_schema: &Value,
1237 root_schema: &Value,
1238 locator: &JsonSourceLocator,
1239 result: &mut ValidationResult,
1240 path: &str,
1241 visited_refs: &mut HashSet<String>,
1242 depth: usize,
1243 ) {
1244 if let Value::Object(def_obj) = def_schema {
1245 if def_obj.is_empty() {
1247 result.add_error(ValidationError::schema_error(
1248 SchemaErrorCode::SchemaDefinitionMissingType,
1249 "Definition must have type, $ref, definitions, or composition",
1250 path,
1251 locator.get_location(path),
1252 ));
1253 return;
1254 }
1255
1256 let has_type = def_obj.contains_key("type");
1257 let has_ref = def_obj.contains_key("$ref");
1258 let has_definitions = def_obj.contains_key("definitions");
1259 let has_composition = def_obj.keys().any(|k|
1260 ["allOf", "anyOf", "oneOf", "not", "if"].contains(&k.as_str())
1261 );
1262
1263 if has_type || has_ref || has_definitions || has_composition {
1264 self.validate_schema_internal(
1266 def_schema,
1267 root_schema,
1268 locator,
1269 result,
1270 path,
1271 false,
1272 visited_refs,
1273 depth + 1,
1274 );
1275 } else {
1276 let is_namespace = def_obj.values().all(|v| {
1279 if let Value::Object(child) = v {
1280 child.contains_key("type")
1281 || child.contains_key("$ref")
1282 || child.contains_key("definitions")
1283 || child.keys().any(|k| ["allOf", "anyOf", "oneOf"].contains(&k.as_str()))
1284 || child.values().all(|cv| cv.is_object()) } else {
1286 false
1287 }
1288 });
1289
1290 if is_namespace {
1291 for (child_name, child_schema) in def_obj {
1293 let child_path = format!("{}/{}", path, child_name);
1294 self.validate_definition_or_namespace(child_schema, root_schema, locator, result, &child_path, visited_refs, depth + 1);
1295 }
1296 } else {
1297 result.add_error(ValidationError::schema_error(
1299 SchemaErrorCode::SchemaDefinitionMissingType,
1300 "Definition must have type, $ref, definitions, or composition",
1301 path,
1302 locator.get_location(path),
1303 ));
1304 }
1305 }
1306 } else {
1307 result.add_error(ValidationError::schema_error(
1308 SchemaErrorCode::SchemaDefinitionInvalid,
1309 "Definition must be an object",
1310 path,
1311 locator.get_location(path),
1312 ));
1313 }
1314 }
1315
1316 fn validate_enum(
1318 &self,
1319 enum_val: &Value,
1320 locator: &JsonSourceLocator,
1321 result: &mut ValidationResult,
1322 path: &str,
1323 ) {
1324 let enum_path = format!("{}/enum", path);
1325
1326 match enum_val {
1327 Value::Array(arr) => {
1328 if arr.is_empty() {
1329 result.add_error(ValidationError::schema_error(
1330 SchemaErrorCode::SchemaEnumEmpty,
1331 "enum cannot be empty",
1332 &enum_path,
1333 locator.get_location(&enum_path),
1334 ));
1335 return;
1336 }
1337
1338 let mut seen = Vec::new();
1340 for (i, item) in arr.iter().enumerate() {
1341 let item_str = item.to_string();
1342 if seen.contains(&item_str) {
1343 result.add_error(ValidationError::schema_error(
1344 SchemaErrorCode::SchemaEnumDuplicates,
1345 "enum contains duplicate values",
1346 &format!("{}/{}", enum_path, i),
1347 locator.get_location(&format!("{}/{}", enum_path, i)),
1348 ));
1349 } else {
1350 seen.push(item_str);
1351 }
1352 }
1353 }
1354 _ => {
1355 result.add_error(ValidationError::schema_error(
1356 SchemaErrorCode::SchemaEnumNotArray,
1357 "enum must be an array",
1358 &enum_path,
1359 locator.get_location(&enum_path),
1360 ));
1361 }
1362 }
1363 }
1364
1365 fn validate_composition(
1367 &self,
1368 obj: &serde_json::Map<String, Value>,
1369 root_schema: &Value,
1370 locator: &JsonSourceLocator,
1371 result: &mut ValidationResult,
1372 path: &str,
1373 _enabled_extensions: &HashSet<&str>,
1374 visited_refs: &mut HashSet<String>,
1375 depth: usize,
1376 ) {
1377 if let Some(all_of) = obj.get("allOf") {
1379 self.validate_composition_array(all_of, "allOf", root_schema, locator, result, path, visited_refs, depth);
1380 }
1381
1382 if let Some(any_of) = obj.get("anyOf") {
1384 self.validate_composition_array(any_of, "anyOf", root_schema, locator, result, path, visited_refs, depth);
1385 }
1386
1387 if let Some(one_of) = obj.get("oneOf") {
1389 self.validate_composition_array(one_of, "oneOf", root_schema, locator, result, path, visited_refs, depth);
1390 }
1391
1392 if let Some(not) = obj.get("not") {
1394 let not_path = format!("{}/not", path);
1395 self.validate_schema_internal(not, root_schema, locator, result, ¬_path, false, visited_refs, depth + 1);
1396 }
1397
1398 if let Some(if_schema) = obj.get("if") {
1400 let if_path = format!("{}/if", path);
1401 self.validate_schema_internal(if_schema, root_schema, locator, result, &if_path, false, visited_refs, depth + 1);
1402 }
1403
1404 if let Some(then_schema) = obj.get("then") {
1405 if !obj.contains_key("if") {
1406 result.add_error(ValidationError::schema_error(
1407 SchemaErrorCode::SchemaThenWithoutIf,
1408 "then requires if",
1409 &format!("{}/then", path),
1410 locator.get_location(&format!("{}/then", path)),
1411 ));
1412 }
1413 let then_path = format!("{}/then", path);
1414 self.validate_schema_internal(then_schema, root_schema, locator, result, &then_path, false, visited_refs, depth + 1);
1415 }
1416
1417 if let Some(else_schema) = obj.get("else") {
1418 if !obj.contains_key("if") {
1419 result.add_error(ValidationError::schema_error(
1420 SchemaErrorCode::SchemaElseWithoutIf,
1421 "else requires if",
1422 &format!("{}/else", path),
1423 locator.get_location(&format!("{}/else", path)),
1424 ));
1425 }
1426 let else_path = format!("{}/else", path);
1427 self.validate_schema_internal(else_schema, root_schema, locator, result, &else_path, false, visited_refs, depth + 1);
1428 }
1429 }
1430
1431 fn validate_composition_array(
1433 &self,
1434 value: &Value,
1435 keyword: &str,
1436 root_schema: &Value,
1437 locator: &JsonSourceLocator,
1438 result: &mut ValidationResult,
1439 path: &str,
1440 visited_refs: &mut HashSet<String>,
1441 depth: usize,
1442 ) {
1443 let keyword_path = format!("{}/{}", path, keyword);
1444
1445 match value {
1446 Value::Array(arr) => {
1447 for (i, item) in arr.iter().enumerate() {
1448 let item_path = format!("{}/{}", keyword_path, i);
1449 self.validate_schema_internal(item, root_schema, locator, result, &item_path, false, visited_refs, depth + 1);
1450 }
1451 }
1452 _ => {
1453 let code = match keyword {
1454 "allOf" => SchemaErrorCode::SchemaAllOfNotArray,
1455 "anyOf" => SchemaErrorCode::SchemaAnyOfNotArray,
1456 "oneOf" => SchemaErrorCode::SchemaOneOfNotArray,
1457 _ => SchemaErrorCode::SchemaAllOfNotArray,
1458 };
1459 result.add_error(ValidationError::schema_error(
1460 code,
1461 format!("{} must be an array", keyword),
1462 &keyword_path,
1463 locator.get_location(&keyword_path),
1464 ));
1465 }
1466 }
1467 }
1468
1469 fn check_extension_keywords(
1471 &self,
1472 obj: &serde_json::Map<String, Value>,
1473 locator: &JsonSourceLocator,
1474 result: &mut ValidationResult,
1475 path: &str,
1476 enabled_extensions: &HashSet<&str>,
1477 ) {
1478 let validation_enabled = enabled_extensions.contains("JSONStructureValidation");
1479 let composition_enabled = enabled_extensions.contains("JSONStructureConditionalComposition");
1480
1481 for (key, _) in obj {
1482 if VALIDATION_EXTENSION_KEYWORDS.contains(&key.as_str()) && !validation_enabled {
1484 result.add_error(ValidationError::schema_warning(
1485 SchemaErrorCode::SchemaExtensionKeywordWithoutUses,
1486 format!(
1487 "Validation extension keyword '{}' is used but validation extensions are not enabled. \
1488 Add '\"$uses\": [\"JSONStructureValidation\"]' to enable validation, or this keyword will be ignored.",
1489 key
1490 ),
1491 &format!("{}/{}", path, key),
1492 locator.get_location(&format!("{}/{}", path, key)),
1493 ));
1494 }
1495
1496 if COMPOSITION_KEYWORDS.contains(&key.as_str()) && !composition_enabled {
1498 result.add_error(ValidationError::schema_warning(
1499 SchemaErrorCode::SchemaExtensionKeywordWithoutUses,
1500 format!(
1501 "Conditional composition keyword '{}' is used but extensions are not enabled. \
1502 Add '\"$uses\": [\"JSONStructureConditionalComposition\"]' to enable.",
1503 key
1504 ),
1505 &format!("{}/{}", path, key),
1506 locator.get_location(&format!("{}/{}", path, key)),
1507 ));
1508 }
1509 }
1510 }
1511}
1512
1513#[cfg(test)]
1514mod tests {
1515 use super::*;
1516
1517 #[test]
1518 fn test_valid_simple_schema() {
1519 let schema = r#"{
1520 "$id": "https://example.com/schema",
1521 "name": "TestSchema",
1522 "type": "string"
1523 }"#;
1524
1525 let validator = SchemaValidator::new();
1526 let result = validator.validate(schema);
1527 assert!(result.is_valid());
1528 }
1529
1530 #[test]
1531 fn test_missing_id() {
1532 let schema = r#"{
1533 "name": "TestSchema",
1534 "type": "string"
1535 }"#;
1536
1537 let validator = SchemaValidator::new();
1538 let result = validator.validate(schema);
1539 assert!(!result.is_valid());
1540 }
1541
1542 #[test]
1543 fn test_missing_name_with_type() {
1544 let schema = r#"{
1545 "$id": "https://example.com/schema",
1546 "type": "string"
1547 }"#;
1548
1549 let validator = SchemaValidator::new();
1550 let result = validator.validate(schema);
1551 assert!(!result.is_valid());
1552 }
1553
1554 #[test]
1555 fn test_invalid_type() {
1556 let schema = r#"{
1557 "$id": "https://example.com/schema",
1558 "name": "TestSchema",
1559 "type": "invalid_type"
1560 }"#;
1561
1562 let validator = SchemaValidator::new();
1563 let result = validator.validate(schema);
1564 assert!(!result.is_valid());
1565 }
1566
1567 #[test]
1568 fn test_array_missing_items() {
1569 let schema = r#"{
1570 "$id": "https://example.com/schema",
1571 "name": "TestSchema",
1572 "type": "array"
1573 }"#;
1574
1575 let validator = SchemaValidator::new();
1576 let result = validator.validate(schema);
1577 assert!(!result.is_valid());
1578 }
1579
1580 #[test]
1581 fn test_map_missing_values() {
1582 let schema = r#"{
1583 "$id": "https://example.com/schema",
1584 "name": "TestSchema",
1585 "type": "map"
1586 }"#;
1587
1588 let validator = SchemaValidator::new();
1589 let result = validator.validate(schema);
1590 assert!(!result.is_valid());
1591 }
1592
1593 #[test]
1594 fn test_tuple_valid() {
1595 let schema = r#"{
1596 "$id": "https://example.com/schema",
1597 "name": "TestSchema",
1598 "type": "tuple",
1599 "properties": {
1600 "first": { "type": "string" },
1601 "second": { "type": "int32" }
1602 },
1603 "tuple": ["first", "second"]
1604 }"#;
1605
1606 let validator = SchemaValidator::new();
1607 let result = validator.validate(schema);
1608 assert!(result.is_valid());
1609 }
1610
1611 #[test]
1612 fn test_choice_valid() {
1613 let schema = r#"{
1614 "$id": "https://example.com/schema",
1615 "name": "TestSchema",
1616 "type": "choice",
1617 "selector": "kind",
1618 "choices": {
1619 "text": { "type": "string" },
1620 "number": { "type": "int32" }
1621 }
1622 }"#;
1623
1624 let validator = SchemaValidator::new();
1625 let result = validator.validate(schema);
1626 assert!(result.is_valid());
1627 }
1628
1629 #[test]
1630 fn test_enum_empty() {
1631 let schema = r#"{
1632 "$id": "https://example.com/schema",
1633 "name": "TestSchema",
1634 "type": "string",
1635 "enum": []
1636 }"#;
1637
1638 let validator = SchemaValidator::new();
1639 let result = validator.validate(schema);
1640 assert!(!result.is_valid());
1641 }
1642
1643 #[test]
1644 fn test_ref_to_definition() {
1645 let schema = r##"{
1646 "$id": "https://example.com/schema",
1647 "name": "TestSchema",
1648 "type": "object",
1649 "definitions": {
1650 "Inner": {
1651 "type": "string"
1652 }
1653 },
1654 "properties": {
1655 "value": { "type": { "$ref": "#/definitions/Inner" } }
1656 }
1657 }"##;
1658
1659 let validator = SchemaValidator::new();
1660 let result = validator.validate(schema);
1661 for err in result.all_errors() {
1662 println!("Error: {:?}", err);
1663 }
1664 assert!(result.is_valid(), "Schema with valid ref should pass");
1665 }
1666
1667 #[test]
1668 fn test_ref_undefined() {
1669 let schema = r##"{
1670 "$id": "https://example.com/schema",
1671 "name": "TestSchema",
1672 "type": "object",
1673 "properties": {
1674 "value": { "type": { "$ref": "#/definitions/Undefined" } }
1675 }
1676 }"##;
1677
1678 let validator = SchemaValidator::new();
1679 let result = validator.validate(schema);
1680 assert!(!result.is_valid(), "Schema with undefined ref should fail");
1681 }
1682
1683 #[test]
1684 fn test_union_type() {
1685 let schema = r##"{
1686 "$id": "https://example.com/schema",
1687 "name": "TestSchema",
1688 "type": "object",
1689 "properties": {
1690 "value": { "type": ["string", "null"] }
1691 }
1692 }"##;
1693
1694 let validator = SchemaValidator::new();
1695 let result = validator.validate(schema);
1696 for err in result.all_errors() {
1697 println!("Error: {:?}", err);
1698 }
1699 assert!(result.is_valid(), "Schema with union type should pass");
1700 }
1701}