1use regex::Regex;
15use serde_json::Value;
16use std::fmt;
17
18#[derive(Debug, Clone)]
20pub struct ValidationError {
21 pub path: String,
23 pub message: String,
25}
26
27impl fmt::Display for ValidationError {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 write!(f, "{}: {}", self.path, self.message)
30 }
31}
32
33impl std::error::Error for ValidationError {}
34
35pub type ValidationResult = Result<(), Vec<ValidationError>>;
37
38pub fn validate(schema: &Value, value: &Value) -> ValidationResult {
72 let mut errors = Vec::new();
73 validate_internal(schema, value, "root", &mut errors);
74
75 if errors.is_empty() {
76 Ok(())
77 } else {
78 Err(errors)
79 }
80}
81
82pub fn validate_strict(schema: &Value, value: &Value) -> ValidationResult {
119 let strict_schema = make_strict_schema(schema);
121 validate(&strict_schema, value)
122}
123
124fn make_strict_schema(schema: &Value) -> Value {
126 match schema {
127 Value::Object(obj) => {
128 let mut new_obj = obj.clone();
129
130 if let Some(type_val) = obj.get("type") {
133 let is_object_type = type_val == "object"
134 || type_val
135 .as_array()
136 .is_some_and(|arr| arr.iter().any(|t| t == "object"));
137
138 if is_object_type && !obj.contains_key("additionalProperties") {
139 new_obj.insert("additionalProperties".to_string(), Value::Bool(false));
140 }
141 }
142
143 if let Some(Value::Object(props)) = obj.get("properties") {
145 let strict_props: serde_json::Map<String, Value> = props
146 .iter()
147 .map(|(k, v)| (k.clone(), make_strict_schema(v)))
148 .collect();
149 new_obj.insert("properties".to_string(), Value::Object(strict_props));
150 }
151
152 if let Some(additional) = obj.get("additionalProperties") {
154 if additional.is_object() {
155 new_obj.insert(
156 "additionalProperties".to_string(),
157 make_strict_schema(additional),
158 );
159 }
160 }
161
162 if let Some(items) = obj.get("items") {
164 new_obj.insert("items".to_string(), make_strict_schema(items));
165 }
166
167 if let Some(Value::Array(arr)) = obj.get("prefixItems") {
169 let strict_items: Vec<Value> = arr.iter().map(make_strict_schema).collect();
170 new_obj.insert("prefixItems".to_string(), Value::Array(strict_items));
171 }
172
173 Value::Object(new_obj)
174 }
175 Value::Array(arr) => {
176 Value::Array(arr.iter().map(make_strict_schema).collect())
178 }
179 _ => schema.clone(),
180 }
181}
182
183fn validate_internal(schema: &Value, value: &Value, path: &str, errors: &mut Vec<ValidationError>) {
185 if let Some(b) = schema.as_bool() {
187 if !b {
188 errors.push(ValidationError {
189 path: path.to_string(),
190 message: "schema rejects all values".to_string(),
191 });
192 }
193 return;
194 }
195
196 let Some(schema_obj) = schema.as_object() else {
198 return; };
200
201 if let Some(type_val) = schema_obj.get("type") {
203 if !validate_type(type_val, value) {
204 let expected = type_val
205 .as_str()
206 .map(String::from)
207 .or_else(|| type_val.as_array().map(|arr| format!("{arr:?}")))
208 .unwrap_or_else(|| "unknown".to_string());
209 errors.push(ValidationError {
210 path: path.to_string(),
211 message: format!("expected type {expected}, got {}", json_type_name(value)),
212 });
213 return; }
215 }
216
217 if let Some(enum_val) = schema_obj.get("enum") {
219 if let Some(enum_arr) = enum_val.as_array() {
220 if !enum_arr.contains(value) {
221 errors.push(ValidationError {
222 path: path.to_string(),
223 message: format!("value must be one of: {enum_arr:?}"),
224 });
225 }
226 }
227 }
228
229 if let Some(const_val) = schema_obj.get("const") {
231 if value != const_val {
232 errors.push(ValidationError {
233 path: path.to_string(),
234 message: format!("value must equal {const_val}"),
235 });
236 }
237 }
238
239 match value {
241 Value::Object(obj) => {
242 validate_object(schema_obj, obj, path, errors);
243 }
244 Value::Array(arr) => {
245 validate_array(schema_obj, arr, path, errors);
246 }
247 Value::String(s) => {
248 validate_string(schema_obj, s, path, errors);
249 }
250 Value::Number(n) => {
251 validate_number(schema_obj, n, path, errors);
252 }
253 _ => {}
254 }
255}
256
257fn validate_type(type_val: &Value, value: &Value) -> bool {
259 match type_val {
260 Value::String(t) => matches_type(t, value),
261 Value::Array(types) => types.iter().any(|t| {
262 t.as_str()
263 .is_some_and(|type_str| matches_type(type_str, value))
264 }),
265 _ => true, }
267}
268
269fn matches_type(type_name: &str, value: &Value) -> bool {
271 match type_name {
272 "string" => value.is_string(),
273 "number" => value.is_number(),
274 "integer" => value.is_i64() || value.is_u64(),
275 "boolean" => value.is_boolean(),
276 "object" => value.is_object(),
277 "array" => value.is_array(),
278 "null" => value.is_null(),
279 _ => true, }
281}
282
283fn json_type_name(value: &Value) -> &'static str {
285 match value {
286 Value::Null => "null",
287 Value::Bool(_) => "boolean",
288 Value::Number(n) => {
289 if n.is_i64() || n.is_u64() {
290 "integer"
291 } else {
292 "number"
293 }
294 }
295 Value::String(_) => "string",
296 Value::Array(_) => "array",
297 Value::Object(_) => "object",
298 }
299}
300
301fn validate_object(
303 schema: &serde_json::Map<String, Value>,
304 obj: &serde_json::Map<String, Value>,
305 path: &str,
306 errors: &mut Vec<ValidationError>,
307) {
308 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
310 for req in required {
311 if let Some(req_name) = req.as_str() {
312 if !obj.contains_key(req_name) {
313 errors.push(ValidationError {
314 path: path.to_string(),
315 message: format!("missing required field: {req_name}"),
316 });
317 }
318 }
319 }
320 }
321
322 if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
324 for (key, value) in obj {
325 if let Some(prop_schema) = properties.get(key) {
326 let prop_path = format!("{path}.{key}");
327 validate_internal(prop_schema, value, &prop_path, errors);
328 }
329 }
330 }
331
332 if let Some(additional) = schema.get("additionalProperties") {
334 let properties = schema.get("properties").and_then(|v| v.as_object());
336
337 for (key, value) in obj {
338 let is_defined_property = properties.is_some_and(|p| p.contains_key(key));
340 if !is_defined_property {
341 match additional {
342 Value::Bool(false) => {
343 errors.push(ValidationError {
344 path: path.to_string(),
345 message: format!("additional property not allowed: {key}"),
346 });
347 }
348 Value::Object(_) => {
349 let prop_path = format!("{path}.{key}");
350 validate_internal(additional, value, &prop_path, errors);
351 }
352 _ => {}
353 }
354 }
355 }
356 }
357
358 if let Some(min) = schema
360 .get("minProperties")
361 .and_then(serde_json::Value::as_u64)
362 {
363 if (obj.len() as u64) < min {
364 errors.push(ValidationError {
365 path: path.to_string(),
366 message: format!("object must have at least {min} properties"),
367 });
368 }
369 }
370 if let Some(max) = schema
371 .get("maxProperties")
372 .and_then(serde_json::Value::as_u64)
373 {
374 if (obj.len() as u64) > max {
375 errors.push(ValidationError {
376 path: path.to_string(),
377 message: format!("object must have at most {max} properties"),
378 });
379 }
380 }
381}
382
383fn validate_array(
385 schema: &serde_json::Map<String, Value>,
386 arr: &[Value],
387 path: &str,
388 errors: &mut Vec<ValidationError>,
389) {
390 let mut prefix_len = 0;
392 if let Some(prefix_items) = schema.get("prefixItems").and_then(|v| v.as_array()) {
393 prefix_len = prefix_items.len();
394 for (i, item_schema) in prefix_items.iter().enumerate() {
395 if let Some(item) = arr.get(i) {
396 let item_path = format!("{path}[{i}]");
397 validate_internal(item_schema, item, &item_path, errors);
398 }
399 }
400 }
401
402 if let Some(items_schema) = schema.get("items") {
404 if items_schema.is_array() && prefix_len == 0 {
406 if let Some(items_arr) = items_schema.as_array() {
407 for (i, item_schema) in items_arr.iter().enumerate() {
408 if let Some(item) = arr.get(i) {
409 let item_path = format!("{path}[{i}]");
410 validate_internal(item_schema, item, &item_path, errors);
411 }
412 }
413 }
415 } else if items_schema.is_object() || items_schema.is_boolean() {
416 for (i, item) in arr.iter().enumerate().skip(prefix_len) {
418 let item_path = format!("{path}[{i}]");
419 validate_internal(items_schema, item, &item_path, errors);
420 }
421 }
422 }
423
424 if let Some(min) = schema.get("minItems").and_then(serde_json::Value::as_u64) {
426 if (arr.len() as u64) < min {
427 errors.push(ValidationError {
428 path: path.to_string(),
429 message: format!("array must have at least {min} items"),
430 });
431 }
432 }
433 if let Some(max) = schema.get("maxItems").and_then(serde_json::Value::as_u64) {
434 if (arr.len() as u64) > max {
435 errors.push(ValidationError {
436 path: path.to_string(),
437 message: format!("array must have at most {max} items"),
438 });
439 }
440 }
441
442 if schema
444 .get("uniqueItems")
445 .and_then(serde_json::Value::as_bool)
446 .unwrap_or(false)
447 {
448 let mut seen = std::collections::HashSet::with_capacity(arr.len());
451 for (i, item) in arr.iter().enumerate() {
452 let key = serde_json::to_string(item).unwrap_or_default();
455 if !seen.insert(key) {
456 errors.push(ValidationError {
457 path: format!("{path}[{i}]"),
458 message: "duplicate item in array".to_string(),
459 });
460 }
461 }
462 }
463}
464
465fn validate_string(
467 schema: &serde_json::Map<String, Value>,
468 s: &str,
469 path: &str,
470 errors: &mut Vec<ValidationError>,
471) {
472 let len = s.chars().count();
474 if let Some(min) = schema.get("minLength").and_then(serde_json::Value::as_u64) {
475 if (len as u64) < min {
476 errors.push(ValidationError {
477 path: path.to_string(),
478 message: format!("string must be at least {min} characters"),
479 });
480 }
481 }
482 if let Some(max) = schema.get("maxLength").and_then(serde_json::Value::as_u64) {
483 if (len as u64) > max {
484 errors.push(ValidationError {
485 path: path.to_string(),
486 message: format!("string must be at most {max} characters"),
487 });
488 }
489 }
490
491 if let Some(pattern) = schema.get("pattern").and_then(serde_json::Value::as_str) {
493 match Regex::new(pattern) {
494 Ok(re) => {
495 if !re.is_match(s) {
496 errors.push(ValidationError {
497 path: path.to_string(),
498 message: format!("string does not match pattern {pattern:?}"),
499 });
500 }
501 }
502 Err(e) => {
503 errors.push(ValidationError {
505 path: path.to_string(),
506 message: format!("invalid schema pattern {pattern:?}: {e}"),
507 });
508 }
509 }
510 }
511}
512
513fn validate_number(
515 schema: &serde_json::Map<String, Value>,
516 n: &serde_json::Number,
517 path: &str,
518 errors: &mut Vec<ValidationError>,
519) {
520 let val = n.as_f64().unwrap_or(0.0);
521
522 if let Some(min) = schema.get("minimum").and_then(serde_json::Value::as_f64) {
524 if val < min {
525 errors.push(ValidationError {
526 path: path.to_string(),
527 message: format!("value must be >= {min}"),
528 });
529 }
530 }
531 if let Some(max) = schema.get("maximum").and_then(serde_json::Value::as_f64) {
532 if val > max {
533 errors.push(ValidationError {
534 path: path.to_string(),
535 message: format!("value must be <= {max}"),
536 });
537 }
538 }
539
540 if let Some(min) = schema
542 .get("exclusiveMinimum")
543 .and_then(serde_json::Value::as_f64)
544 {
545 if val <= min {
546 errors.push(ValidationError {
547 path: path.to_string(),
548 message: format!("value must be > {min}"),
549 });
550 }
551 }
552 if let Some(max) = schema
553 .get("exclusiveMaximum")
554 .and_then(serde_json::Value::as_f64)
555 {
556 if val >= max {
557 errors.push(ValidationError {
558 path: path.to_string(),
559 message: format!("value must be < {max}"),
560 });
561 }
562 }
563
564 if let Some(multiple) = schema.get("multipleOf").and_then(serde_json::Value::as_f64) {
566 if multiple != 0.0 && (val % multiple).abs() > f64::EPSILON {
567 errors.push(ValidationError {
568 path: path.to_string(),
569 message: format!("value must be a multiple of {multiple}"),
570 });
571 }
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use serde_json::json;
579
580 #[test]
581 fn test_type_validation_string() {
582 let schema = json!({"type": "string"});
583 assert!(validate(&schema, &json!("hello")).is_ok());
584 assert!(validate(&schema, &json!(123)).is_err());
585 }
586
587 #[test]
588 fn test_type_validation_number() {
589 let schema = json!({"type": "number"});
590 assert!(validate(&schema, &json!(123)).is_ok());
591 assert!(validate(&schema, &json!(12.5)).is_ok());
592 assert!(validate(&schema, &json!("hello")).is_err());
593 }
594
595 #[test]
596 fn test_type_validation_integer() {
597 let schema = json!({"type": "integer"});
598 assert!(validate(&schema, &json!(123)).is_ok());
599 assert!(validate(&schema, &json!(12.5)).is_err());
600 }
601
602 #[test]
603 fn test_type_validation_boolean() {
604 let schema = json!({"type": "boolean"});
605 assert!(validate(&schema, &json!(true)).is_ok());
606 assert!(validate(&schema, &json!(false)).is_ok());
607 assert!(validate(&schema, &json!(1)).is_err());
608 }
609
610 #[test]
611 fn test_type_validation_object() {
612 let schema = json!({"type": "object"});
613 assert!(validate(&schema, &json!({})).is_ok());
614 assert!(validate(&schema, &json!({"a": 1})).is_ok());
615 assert!(validate(&schema, &json!([])).is_err());
616 }
617
618 #[test]
619 fn test_type_validation_array() {
620 let schema = json!({"type": "array"});
621 assert!(validate(&schema, &json!([])).is_ok());
622 assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
623 assert!(validate(&schema, &json!({})).is_err());
624 }
625
626 #[test]
627 fn test_type_validation_null() {
628 let schema = json!({"type": "null"});
629 assert!(validate(&schema, &json!(null)).is_ok());
630 assert!(validate(&schema, &json!(0)).is_err());
631 }
632
633 #[test]
634 fn test_type_validation_union() {
635 let schema = json!({"type": ["string", "number"]});
636 assert!(validate(&schema, &json!("hello")).is_ok());
637 assert!(validate(&schema, &json!(123)).is_ok());
638 assert!(validate(&schema, &json!(true)).is_err());
639 }
640
641 #[test]
642 fn test_required_fields() {
643 let schema = json!({
644 "type": "object",
645 "properties": {
646 "name": {"type": "string"},
647 "age": {"type": "integer"}
648 },
649 "required": ["name"]
650 });
651
652 assert!(validate(&schema, &json!({"name": "Alice"})).is_ok());
653 assert!(validate(&schema, &json!({"name": "Alice", "age": 30})).is_ok());
654 assert!(validate(&schema, &json!({"age": 30})).is_err());
655 assert!(validate(&schema, &json!({})).is_err());
656 }
657
658 #[test]
659 fn test_enum_validation() {
660 let schema = json!({"enum": ["red", "green", "blue"]});
661 assert!(validate(&schema, &json!("red")).is_ok());
662 assert!(validate(&schema, &json!("yellow")).is_err());
663 }
664
665 #[test]
666 fn test_const_validation() {
667 let schema = json!({"const": "fixed"});
668 assert!(validate(&schema, &json!("fixed")).is_ok());
669 assert!(validate(&schema, &json!("other")).is_err());
670 }
671
672 #[test]
673 fn test_string_length() {
674 let schema = json!({
675 "type": "string",
676 "minLength": 2,
677 "maxLength": 5
678 });
679
680 assert!(validate(&schema, &json!("ab")).is_ok());
681 assert!(validate(&schema, &json!("abcde")).is_ok());
682 assert!(validate(&schema, &json!("a")).is_err());
683 assert!(validate(&schema, &json!("abcdef")).is_err());
684 }
685
686 #[test]
687 fn test_string_pattern() {
688 let schema = json!({
689 "type": "string",
690 "pattern": "^[a-z]+$"
691 });
692
693 assert!(validate(&schema, &json!("hello")).is_ok());
694 assert!(validate(&schema, &json!("Hello")).is_err());
695 assert!(validate(&schema, &json!("hello123")).is_err());
696 }
697
698 #[test]
699 fn test_string_pattern_invalid_regex_is_error() {
700 let schema = json!({
701 "type": "string",
702 "pattern": "("
703 });
704
705 assert!(validate(&schema, &json!("anything")).is_err());
706 }
707
708 #[test]
709 fn test_number_range() {
710 let schema = json!({
711 "type": "number",
712 "minimum": 0,
713 "maximum": 100
714 });
715
716 assert!(validate(&schema, &json!(0)).is_ok());
717 assert!(validate(&schema, &json!(50)).is_ok());
718 assert!(validate(&schema, &json!(100)).is_ok());
719 assert!(validate(&schema, &json!(-1)).is_err());
720 assert!(validate(&schema, &json!(101)).is_err());
721 }
722
723 #[test]
724 fn test_number_exclusive_range() {
725 let schema = json!({
726 "type": "number",
727 "exclusiveMinimum": 0,
728 "exclusiveMaximum": 10
729 });
730
731 assert!(validate(&schema, &json!(1)).is_ok());
732 assert!(validate(&schema, &json!(9)).is_ok());
733 assert!(validate(&schema, &json!(0)).is_err());
734 assert!(validate(&schema, &json!(10)).is_err());
735 }
736
737 #[test]
738 fn test_array_items() {
739 let schema = json!({
740 "type": "array",
741 "items": {"type": "integer"}
742 });
743
744 assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
745 assert!(validate(&schema, &json!([])).is_ok());
746 assert!(validate(&schema, &json!([1, "two", 3])).is_err());
747 }
748
749 #[test]
750 fn test_array_length() {
751 let schema = json!({
752 "type": "array",
753 "minItems": 1,
754 "maxItems": 3
755 });
756
757 assert!(validate(&schema, &json!([1])).is_ok());
758 assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
759 assert!(validate(&schema, &json!([])).is_err());
760 assert!(validate(&schema, &json!([1, 2, 3, 4])).is_err());
761 }
762
763 #[test]
764 fn test_unique_items() {
765 let schema = json!({
766 "type": "array",
767 "uniqueItems": true
768 });
769
770 assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
771 assert!(validate(&schema, &json!([1, 1, 2])).is_err());
772 }
773
774 #[test]
775 fn test_nested_object() {
776 let schema = json!({
777 "type": "object",
778 "properties": {
779 "person": {
780 "type": "object",
781 "properties": {
782 "name": {"type": "string"},
783 "age": {"type": "integer"}
784 },
785 "required": ["name"]
786 }
787 }
788 });
789
790 assert!(validate(&schema, &json!({"person": {"name": "Alice"}})).is_ok());
791 assert!(validate(&schema, &json!({"person": {"name": "Alice", "age": 30}})).is_ok());
792 assert!(validate(&schema, &json!({"person": {"age": 30}})).is_err());
793 }
794
795 #[test]
796 fn test_additional_properties_false() {
797 let schema = json!({
798 "type": "object",
799 "properties": {
800 "name": {"type": "string"}
801 },
802 "additionalProperties": false
803 });
804
805 assert!(validate(&schema, &json!({"name": "Alice"})).is_ok());
806 assert!(validate(&schema, &json!({})).is_ok());
807 assert!(validate(&schema, &json!({"name": "Alice", "extra": 1})).is_err());
808 }
809
810 #[test]
811 fn test_boolean_schema() {
812 assert!(validate(&json!(true), &json!("anything")).is_ok());
814 assert!(validate(&json!(true), &json!(123)).is_ok());
815
816 assert!(validate(&json!(false), &json!("anything")).is_err());
818 }
819
820 #[test]
821 fn test_multiple_errors() {
822 let schema = json!({
823 "type": "object",
824 "properties": {
825 "name": {"type": "string"},
826 "age": {"type": "integer"}
827 },
828 "required": ["name", "age"]
829 });
830
831 let result = validate(&schema, &json!({}));
832 assert!(result.is_err());
833 let errors = result.unwrap_err();
834 assert_eq!(errors.len(), 2); }
836
837 #[test]
838 fn test_error_path() {
839 let schema = json!({
840 "type": "object",
841 "properties": {
842 "items": {
843 "type": "array",
844 "items": {"type": "integer"}
845 }
846 }
847 });
848
849 let result = validate(&schema, &json!({"items": [1, "two", 3]}));
850 assert!(result.is_err());
851 let errors = result.unwrap_err();
852 assert_eq!(errors.len(), 1);
853 assert_eq!(errors[0].path, "root.items[1]");
854 }
855
856 #[test]
861 fn test_validate_strict_rejects_extra_properties() {
862 let schema = json!({
863 "type": "object",
864 "properties": {
865 "name": {"type": "string"}
866 }
867 });
868
869 assert!(validate(&schema, &json!({"name": "Alice", "extra": 123})).is_ok());
871
872 assert!(validate_strict(&schema, &json!({"name": "Alice", "extra": 123})).is_err());
874
875 assert!(validate_strict(&schema, &json!({"name": "Alice"})).is_ok());
877 }
878
879 #[test]
880 fn test_validate_strict_nested_objects() {
881 let schema = json!({
882 "type": "object",
883 "properties": {
884 "person": {
885 "type": "object",
886 "properties": {
887 "name": {"type": "string"}
888 }
889 }
890 }
891 });
892
893 assert!(
895 validate(
896 &schema,
897 &json!({
898 "person": {"name": "Alice", "age": 30}
899 })
900 )
901 .is_ok()
902 );
903
904 assert!(
906 validate_strict(
907 &schema,
908 &json!({
909 "person": {"name": "Alice", "age": 30}
910 })
911 )
912 .is_err()
913 );
914
915 assert!(
917 validate_strict(
918 &schema,
919 &json!({
920 "person": {"name": "Alice"}
921 })
922 )
923 .is_ok()
924 );
925 }
926
927 #[test]
928 fn test_validate_strict_preserves_explicit_additional_properties() {
929 let schema = json!({
931 "type": "object",
932 "properties": {
933 "name": {"type": "string"}
934 },
935 "additionalProperties": {"type": "integer"}
936 });
937
938 assert!(
940 validate_strict(
941 &schema,
942 &json!({
943 "name": "Alice",
944 "count": 42
945 })
946 )
947 .is_ok()
948 );
949
950 assert!(
952 validate_strict(
953 &schema,
954 &json!({
955 "name": "Alice",
956 "count": "not an integer"
957 })
958 )
959 .is_err()
960 );
961 }
962
963 #[test]
964 fn test_validate_strict_array_items() {
965 let schema = json!({
966 "type": "array",
967 "items": {
968 "type": "object",
969 "properties": {
970 "id": {"type": "integer"}
971 }
972 }
973 });
974
975 assert!(
977 validate(
978 &schema,
979 &json!([
980 {"id": 1, "extra": "value"}
981 ])
982 )
983 .is_ok()
984 );
985
986 assert!(
988 validate_strict(
989 &schema,
990 &json!([
991 {"id": 1, "extra": "value"}
992 ])
993 )
994 .is_err()
995 );
996
997 assert!(
999 validate_strict(
1000 &schema,
1001 &json!([
1002 {"id": 1}
1003 ])
1004 )
1005 .is_ok()
1006 );
1007 }
1008
1009 #[test]
1010 fn test_validate_strict_empty_schema() {
1011 let schema = json!({});
1013
1014 assert!(validate_strict(&schema, &json!({"anything": "goes"})).is_ok());
1016 }
1017
1018 #[test]
1019 fn test_validate_strict_non_object_types() {
1020 let string_schema = json!({"type": "string"});
1022 assert!(validate_strict(&string_schema, &json!("hello")).is_ok());
1023
1024 let number_schema = json!({"type": "number"});
1025 assert!(validate_strict(&number_schema, &json!(42)).is_ok());
1026
1027 let array_schema = json!({"type": "array"});
1028 assert!(validate_strict(&array_schema, &json!([1, 2, 3])).is_ok());
1029 }
1030
1031 #[test]
1036 fn validation_error_display_and_error_trait() {
1037 let err = ValidationError {
1038 path: "root.name".to_string(),
1039 message: "expected string".to_string(),
1040 };
1041 assert_eq!(err.to_string(), "root.name: expected string");
1042 let _: &dyn std::error::Error = &err;
1043 }
1044
1045 #[test]
1046 fn validation_error_debug_and_clone() {
1047 let err = ValidationError {
1048 path: "root".to_string(),
1049 message: "missing".to_string(),
1050 };
1051 let debug = format!("{err:?}");
1052 assert!(debug.contains("ValidationError"));
1053
1054 let cloned = err.clone();
1055 assert_eq!(cloned.path, "root");
1056 assert_eq!(cloned.message, "missing");
1057 }
1058
1059 #[test]
1060 fn json_type_name_all_types() {
1061 assert_eq!(json_type_name(&json!(null)), "null");
1062 assert_eq!(json_type_name(&json!(true)), "boolean");
1063 assert_eq!(json_type_name(&json!(42)), "integer");
1064 assert_eq!(json_type_name(&json!(3.14)), "number");
1065 assert_eq!(json_type_name(&json!("hello")), "string");
1066 assert_eq!(json_type_name(&json!([])), "array");
1067 assert_eq!(json_type_name(&json!({})), "object");
1068 }
1069
1070 #[test]
1071 fn number_multiple_of() {
1072 let schema = json!({"type": "number", "multipleOf": 3});
1073 assert!(validate(&schema, &json!(9)).is_ok());
1074 assert!(validate(&schema, &json!(6)).is_ok());
1075 assert!(validate(&schema, &json!(0)).is_ok());
1076 assert!(validate(&schema, &json!(7)).is_err());
1077 }
1078
1079 #[test]
1080 fn object_min_max_properties() {
1081 let schema = json!({
1082 "type": "object",
1083 "minProperties": 1,
1084 "maxProperties": 2
1085 });
1086
1087 assert!(validate(&schema, &json!({"a": 1})).is_ok());
1088 assert!(validate(&schema, &json!({"a": 1, "b": 2})).is_ok());
1089 assert!(validate(&schema, &json!({})).is_err());
1090 assert!(validate(&schema, &json!({"a": 1, "b": 2, "c": 3})).is_err());
1091 }
1092
1093 #[test]
1094 fn prefix_items_tuple_validation() {
1095 let schema = json!({
1096 "type": "array",
1097 "prefixItems": [
1098 {"type": "string"},
1099 {"type": "integer"}
1100 ]
1101 });
1102
1103 assert!(validate(&schema, &json!(["hello", 42])).is_ok());
1104 assert!(validate(&schema, &json!(["hello", 42, true])).is_ok()); assert!(validate(&schema, &json!([123, "wrong"])).is_err()); }
1107
1108 #[test]
1109 fn prefix_items_with_additional_items_schema() {
1110 let schema = json!({
1111 "type": "array",
1112 "prefixItems": [
1113 {"type": "string"}
1114 ],
1115 "items": {"type": "integer"}
1116 });
1117
1118 assert!(validate(&schema, &json!(["hello", 1, 2])).is_ok());
1120 assert!(validate(&schema, &json!(["hello", "bad"])).is_err());
1121 }
1122
1123 #[test]
1124 fn items_as_array_draft4_fallback() {
1125 let schema = json!({
1127 "type": "array",
1128 "items": [
1129 {"type": "string"},
1130 {"type": "integer"}
1131 ]
1132 });
1133
1134 assert!(validate(&schema, &json!(["hello", 42])).is_ok());
1135 assert!(validate(&schema, &json!([123, "wrong"])).is_err());
1136 }
1137
1138 #[test]
1139 fn additional_properties_as_schema() {
1140 let schema = json!({
1141 "type": "object",
1142 "properties": {
1143 "name": {"type": "string"}
1144 },
1145 "additionalProperties": {"type": "integer"}
1146 });
1147
1148 assert!(validate(&schema, &json!({"name": "Alice", "count": 42})).is_ok());
1149 assert!(validate(&schema, &json!({"name": "Alice", "bad": "string"})).is_err());
1150 }
1151
1152 #[test]
1153 fn strict_schema_with_prefix_items() {
1154 let schema = json!({
1155 "type": "array",
1156 "prefixItems": [
1157 {
1158 "type": "object",
1159 "properties": {
1160 "id": {"type": "integer"}
1161 }
1162 }
1163 ]
1164 });
1165
1166 assert!(validate_strict(&schema, &json!([{"id": 1}])).is_ok());
1168 assert!(validate_strict(&schema, &json!([{"id": 1, "extra": "val"}])).is_err());
1169 }
1170
1171 #[test]
1172 fn strict_schema_with_union_type() {
1173 let schema = json!({
1174 "type": ["object", "null"],
1175 "properties": {
1176 "name": {"type": "string"}
1177 }
1178 });
1179
1180 assert!(validate_strict(&schema, &json!(null)).is_ok());
1182 assert!(validate_strict(&schema, &json!({"name": "Alice"})).is_ok());
1183 assert!(validate_strict(&schema, &json!({"name": "Alice", "extra": 1})).is_err());
1184 }
1185
1186 #[test]
1187 fn unknown_type_in_matches_type_is_permissive() {
1188 let schema = json!({"type": "custom_extension"});
1189 assert!(validate(&schema, &json!("anything")).is_ok());
1191 assert!(validate(&schema, &json!(42)).is_ok());
1192 }
1193
1194 #[test]
1195 fn invalid_schema_not_an_object() {
1196 assert!(validate(&json!(42), &json!("anything")).is_ok());
1198 assert!(validate(&json!("bad_schema"), &json!(123)).is_ok());
1199 }
1200}