1use jsonschema::Validator;
60use serde_json::Value as JsonValue;
61use std::sync::Arc;
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
67pub enum SchemaDraft {
68 Draft4,
70 Draft6,
72 #[default]
74 Draft7,
75 Draft201909,
77 Draft202012,
79}
80
81#[derive(Debug, Clone)]
108pub struct ValidationConfig {
109 pub draft: SchemaDraft,
118
119 pub collect_all_errors: bool,
124
125 pub max_errors: Option<usize>,
130
131 pub validate_formats: bool,
136}
137
138impl Default for ValidationConfig {
139 fn default() -> Self {
140 Self {
141 draft: SchemaDraft::Draft7,
142 collect_all_errors: true,
143 max_errors: None,
144 validate_formats: true,
145 }
146 }
147}
148
149#[derive(Debug, Clone)]
154pub struct ValidationError {
155 pub instance_path: String,
157
158 pub message: String,
160
161 pub schema_path: String,
163}
164
165impl std::fmt::Display for ValidationError {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 write!(
168 f,
169 "Validation error at {}: {} (schema: {})",
170 self.instance_path, self.message, self.schema_path
171 )
172 }
173}
174
175impl std::error::Error for ValidationError {}
176
177#[derive(Debug, Clone)]
181pub struct ValidationResult {
182 pub is_valid: bool,
184
185 pub errors: Vec<ValidationError>,
187}
188
189impl ValidationResult {
190 #[must_use]
192 pub fn valid() -> Self {
193 Self {
194 is_valid: true,
195 errors: Vec::new(),
196 }
197 }
198
199 #[must_use]
201 pub fn invalid(errors: Vec<ValidationError>) -> Self {
202 Self {
203 is_valid: false,
204 errors,
205 }
206 }
207}
208
209#[derive(Debug, Clone, thiserror::Error)]
211pub enum SchemaError {
212 #[error("Invalid JSON Schema: {0}")]
214 InvalidSchema(String),
215
216 #[error("Unresolved schema reference: {0}")]
218 UnresolvedReference(String),
219}
220
221#[derive(Clone)]
254pub struct CompiledSchema {
255 validator: Arc<Validator>,
256 config: ValidationConfig,
257}
258
259impl std::fmt::Debug for CompiledSchema {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 f.debug_struct("CompiledSchema")
262 .field("config", &self.config)
263 .finish_non_exhaustive()
264 }
265}
266
267impl CompiledSchema {
268 pub fn compile(schema: &JsonValue, config: &ValidationConfig) -> Result<Self, SchemaError> {
295 let validator = match config.draft {
296 SchemaDraft::Draft4 => jsonschema::draft4::options()
297 .should_validate_formats(config.validate_formats)
298 .build(schema),
299 SchemaDraft::Draft6 => jsonschema::draft6::options()
300 .should_validate_formats(config.validate_formats)
301 .build(schema),
302 SchemaDraft::Draft7 => jsonschema::draft7::options()
303 .should_validate_formats(config.validate_formats)
304 .build(schema),
305 SchemaDraft::Draft201909 => jsonschema::draft201909::options()
306 .should_validate_formats(config.validate_formats)
307 .build(schema),
308 SchemaDraft::Draft202012 => jsonschema::draft202012::options()
309 .should_validate_formats(config.validate_formats)
310 .build(schema),
311 }
312 .map_err(|e| SchemaError::InvalidSchema(e.to_string()))?;
313
314 Ok(Self {
315 validator: Arc::new(validator),
316 config: config.clone(),
317 })
318 }
319
320 #[must_use]
346 pub fn validate(&self, instance: &JsonValue) -> ValidationResult {
347 if self.validator.is_valid(instance) {
348 return ValidationResult::valid();
349 }
350
351 let max = self.config.max_errors.unwrap_or(usize::MAX);
352 let limit = if self.config.collect_all_errors {
353 max
354 } else {
355 1
356 };
357
358 let collected: Vec<ValidationError> = self
359 .validator
360 .iter_errors(instance)
361 .take(limit)
362 .map(|e| ValidationError {
363 instance_path: e.instance_path.to_string(),
364 message: e.to_string(),
365 schema_path: e.schema_path.to_string(),
366 })
367 .collect();
368
369 ValidationResult::invalid(collected)
370 }
371
372 #[must_use]
385 pub fn is_valid(&self, instance: &JsonValue) -> bool {
386 self.validator.is_valid(instance)
387 }
388}
389
390pub fn validate_json(
419 schema: &JsonValue,
420 instance: &JsonValue,
421 config: &ValidationConfig,
422) -> Result<ValidationResult, SchemaError> {
423 let compiled = CompiledSchema::compile(schema, config)?;
424 Ok(compiled.validate(instance))
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430 use serde_json::json;
431
432 #[test]
433 fn test_validation_config_default() {
434 let config = ValidationConfig::default();
435 assert!(matches!(config.draft, SchemaDraft::Draft7));
436 assert!(config.collect_all_errors);
437 assert!(config.max_errors.is_none());
438 assert!(config.validate_formats);
439 }
440
441 #[test]
442 fn test_compile_valid_schema() {
443 let schema = json!({
444 "type": "object",
445 "properties": {
446 "name": {"type": "string"}
447 }
448 });
449
450 let result = CompiledSchema::compile(&schema, &ValidationConfig::default());
451 assert!(result.is_ok());
452 }
453
454 #[test]
455 fn test_compile_invalid_schema() {
456 let schema = json!({
457 "type": "invalid_type_that_does_not_exist"
458 });
459
460 let result = CompiledSchema::compile(&schema, &ValidationConfig::default());
461 assert!(result.is_err());
462 }
463
464 #[test]
465 fn test_validate_valid_document() {
466 let schema = json!({
467 "type": "object",
468 "properties": {
469 "name": {"type": "string"},
470 "age": {"type": "integer"}
471 },
472 "required": ["name"]
473 });
474
475 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
476
477 let valid = json!({"name": "Alice", "age": 30});
478 let result = compiled.validate(&valid);
479
480 assert!(result.is_valid);
481 assert!(result.errors.is_empty());
482 }
483
484 #[test]
485 fn test_validate_invalid_document() {
486 let schema = json!({
487 "type": "object",
488 "properties": {
489 "name": {"type": "string"},
490 "age": {"type": "integer"}
491 },
492 "required": ["name"]
493 });
494
495 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
496
497 let invalid = json!({"age": "not an integer"});
498 let result = compiled.validate(&invalid);
499
500 assert!(!result.is_valid);
501 assert!(!result.errors.is_empty());
502 }
503
504 #[test]
505 fn test_validate_type_mismatch() {
506 let schema = json!({"type": "string"});
507 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
508
509 assert!(compiled.validate(&json!("hello")).is_valid);
510 assert!(!compiled.validate(&json!(42)).is_valid);
511 assert!(!compiled.validate(&json!(true)).is_valid);
512 assert!(!compiled.validate(&json!(null)).is_valid);
513 }
514
515 #[test]
516 fn test_validate_string_constraints() {
517 let schema = json!({
518 "type": "string",
519 "minLength": 2,
520 "maxLength": 5
521 });
522
523 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
524
525 assert!(!compiled.validate(&json!("a")).is_valid); assert!(compiled.validate(&json!("ab")).is_valid);
527 assert!(compiled.validate(&json!("abcde")).is_valid);
528 assert!(!compiled.validate(&json!("abcdef")).is_valid); }
530
531 #[test]
532 fn test_validate_number_constraints() {
533 let schema = json!({
534 "type": "integer",
535 "minimum": 0,
536 "maximum": 100
537 });
538
539 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
540
541 assert!(!compiled.validate(&json!(-1)).is_valid); assert!(compiled.validate(&json!(0)).is_valid);
543 assert!(compiled.validate(&json!(50)).is_valid);
544 assert!(compiled.validate(&json!(100)).is_valid);
545 assert!(!compiled.validate(&json!(101)).is_valid); }
547
548 #[test]
549 fn test_validate_array_constraints() {
550 let schema = json!({
551 "type": "array",
552 "items": {"type": "integer"},
553 "minItems": 1,
554 "maxItems": 3
555 });
556
557 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
558
559 assert!(!compiled.validate(&json!([])).is_valid); assert!(compiled.validate(&json!([1])).is_valid);
561 assert!(compiled.validate(&json!([1, 2, 3])).is_valid);
562 assert!(!compiled.validate(&json!([1, 2, 3, 4])).is_valid); assert!(!compiled.validate(&json!([1, "string", 3])).is_valid); }
565
566 #[test]
567 fn test_validate_required_properties() {
568 let schema = json!({
569 "type": "object",
570 "properties": {
571 "id": {"type": "integer"},
572 "name": {"type": "string"}
573 },
574 "required": ["id", "name"]
575 });
576
577 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
578
579 assert!(
580 compiled
581 .validate(&json!({"id": 1, "name": "test"}))
582 .is_valid
583 );
584 assert!(!compiled.validate(&json!({"id": 1})).is_valid); assert!(!compiled.validate(&json!({"name": "test"})).is_valid); assert!(!compiled.validate(&json!({})).is_valid); }
588
589 #[test]
590 fn test_validate_ref_resolution() {
591 let schema = json!({
592 "$defs": {
593 "User": {
594 "type": "object",
595 "properties": {
596 "name": {"type": "string"}
597 },
598 "required": ["name"]
599 }
600 },
601 "type": "object",
602 "properties": {
603 "user": {"$ref": "#/$defs/User"}
604 }
605 });
606
607 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
608
609 assert!(
610 compiled
611 .validate(&json!({"user": {"name": "Alice"}}))
612 .is_valid
613 );
614 assert!(!compiled.validate(&json!({"user": {}})).is_valid); }
616
617 #[test]
618 fn test_validate_any_of() {
619 let schema = json!({
620 "anyOf": [
621 {"type": "string"},
622 {"type": "integer"}
623 ]
624 });
625
626 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
627
628 assert!(compiled.validate(&json!("hello")).is_valid);
629 assert!(compiled.validate(&json!(42)).is_valid);
630 assert!(!compiled.validate(&json!(true)).is_valid);
631 }
632
633 #[test]
634 fn test_validate_all_of() {
635 let schema = json!({
636 "allOf": [
637 {"type": "object"},
638 {"required": ["name"]},
639 {"properties": {"name": {"type": "string"}}}
640 ]
641 });
642
643 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
644
645 assert!(compiled.validate(&json!({"name": "test"})).is_valid);
646 assert!(!compiled.validate(&json!({})).is_valid); assert!(!compiled.validate(&json!({"name": 123})).is_valid); }
649
650 #[test]
651 fn test_collect_all_errors() {
652 let schema = json!({
653 "type": "object",
654 "properties": {
655 "a": {"type": "integer"},
656 "b": {"type": "integer"},
657 "c": {"type": "integer"}
658 }
659 });
660
661 let config = ValidationConfig {
662 collect_all_errors: true,
663 ..Default::default()
664 };
665
666 let compiled = CompiledSchema::compile(&schema, &config).unwrap();
667
668 let invalid = json!({
669 "a": "not int",
670 "b": "not int",
671 "c": "not int"
672 });
673
674 let result = compiled.validate(&invalid);
675 assert!(!result.is_valid);
676 assert_eq!(result.errors.len(), 3); }
678
679 #[test]
680 fn test_stop_at_first_error() {
681 let schema = json!({
682 "type": "object",
683 "properties": {
684 "a": {"type": "integer"},
685 "b": {"type": "integer"},
686 "c": {"type": "integer"}
687 }
688 });
689
690 let config = ValidationConfig {
691 collect_all_errors: false,
692 ..Default::default()
693 };
694
695 let compiled = CompiledSchema::compile(&schema, &config).unwrap();
696
697 let invalid = json!({
698 "a": "not int",
699 "b": "not int",
700 "c": "not int"
701 });
702
703 let result = compiled.validate(&invalid);
704 assert!(!result.is_valid);
705 assert_eq!(result.errors.len(), 1); }
707
708 #[test]
709 fn test_max_errors_limit() {
710 let schema = json!({
711 "type": "object",
712 "properties": {
713 "a": {"type": "integer"},
714 "b": {"type": "integer"},
715 "c": {"type": "integer"},
716 "d": {"type": "integer"},
717 "e": {"type": "integer"}
718 }
719 });
720
721 let config = ValidationConfig {
722 collect_all_errors: true,
723 max_errors: Some(2),
724 ..Default::default()
725 };
726
727 let compiled = CompiledSchema::compile(&schema, &config).unwrap();
728
729 let invalid = json!({
730 "a": "x",
731 "b": "x",
732 "c": "x",
733 "d": "x",
734 "e": "x"
735 });
736
737 let result = compiled.validate(&invalid);
738 assert!(!result.is_valid);
739 assert!(result.errors.len() <= 2); }
741
742 #[test]
743 fn test_is_valid_method() {
744 let schema = json!({"type": "string"});
745 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
746
747 assert!(compiled.is_valid(&json!("hello")));
748 assert!(!compiled.is_valid(&json!(42)));
749 }
750
751 #[test]
752 fn test_validation_error_display() {
753 let error = ValidationError {
754 instance_path: "/users/0/email".to_string(),
755 message: "Invalid email format".to_string(),
756 schema_path: "/properties/email/format".to_string(),
757 };
758
759 let display = error.to_string();
760 assert!(display.contains("/users/0/email"));
761 assert!(display.contains("Invalid email format"));
762 }
763
764 #[test]
765 fn test_validate_json_convenience() {
766 let schema = json!({"type": "string"});
767 let instance = json!("hello");
768
769 let result = validate_json(&schema, &instance, &ValidationConfig::default()).unwrap();
770 assert!(result.is_valid);
771 }
772
773 #[test]
774 fn test_compiled_schema_clone() {
775 let schema = json!({"type": "string"});
776 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
777
778 let cloned = compiled.clone();
779 assert!(cloned.is_valid(&json!("test")));
780 }
781
782 #[test]
783 fn test_nested_object_validation() {
784 let schema = json!({
785 "type": "object",
786 "properties": {
787 "user": {
788 "type": "object",
789 "properties": {
790 "profile": {
791 "type": "object",
792 "properties": {
793 "age": {"type": "integer", "minimum": 0}
794 }
795 }
796 }
797 }
798 }
799 });
800
801 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
802
803 assert!(
804 compiled
805 .validate(&json!({"user": {"profile": {"age": 25}}}))
806 .is_valid
807 );
808 assert!(
809 !compiled
810 .validate(&json!({"user": {"profile": {"age": -5}}}))
811 .is_valid
812 );
813 }
814
815 #[test]
816 fn test_pattern_validation() {
817 let schema = json!({
818 "type": "string",
819 "pattern": "^[a-z]+$"
820 });
821
822 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
823
824 assert!(compiled.validate(&json!("abc")).is_valid);
825 assert!(!compiled.validate(&json!("ABC")).is_valid);
826 assert!(!compiled.validate(&json!("abc123")).is_valid);
827 }
828
829 #[test]
830 fn test_enum_validation() {
831 let schema = json!({
832 "enum": ["red", "green", "blue"]
833 });
834
835 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
836
837 assert!(compiled.validate(&json!("red")).is_valid);
838 assert!(compiled.validate(&json!("green")).is_valid);
839 assert!(compiled.validate(&json!("blue")).is_valid);
840 assert!(!compiled.validate(&json!("yellow")).is_valid);
841 }
842
843 #[test]
844 fn test_const_validation() {
845 let schema = json!({
846 "const": "fixed_value"
847 });
848
849 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
850
851 assert!(compiled.validate(&json!("fixed_value")).is_valid);
852 assert!(!compiled.validate(&json!("other")).is_valid);
853 }
854
855 #[test]
856 fn test_additional_properties_false() {
857 let schema = json!({
858 "type": "object",
859 "properties": {
860 "name": {"type": "string"}
861 },
862 "additionalProperties": false
863 });
864
865 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
866
867 assert!(compiled.validate(&json!({"name": "test"})).is_valid);
868 assert!(
869 !compiled
870 .validate(&json!({"name": "test", "extra": "field"}))
871 .is_valid
872 );
873 }
874
875 #[test]
876 fn test_error_paths() {
877 let schema = json!({
878 "type": "object",
879 "properties": {
880 "users": {
881 "type": "array",
882 "items": {
883 "type": "object",
884 "properties": {
885 "age": {"type": "integer"}
886 }
887 }
888 }
889 }
890 });
891
892 let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
893
894 let invalid = json!({
895 "users": [
896 {"age": 25},
897 {"age": "not an integer"}
898 ]
899 });
900
901 let result = compiled.validate(&invalid);
902 assert!(!result.is_valid);
903 assert!(!result.errors.is_empty());
904 assert!(result.errors[0].instance_path.contains("users"));
906 }
907
908 #[test]
909 fn test_draft_versions() {
910 let schema = json!({"type": "string"});
911
912 let config_d4 = ValidationConfig {
914 draft: SchemaDraft::Draft4,
915 ..Default::default()
916 };
917 assert!(CompiledSchema::compile(&schema, &config_d4).is_ok());
918
919 let config_d6 = ValidationConfig {
921 draft: SchemaDraft::Draft6,
922 ..Default::default()
923 };
924 assert!(CompiledSchema::compile(&schema, &config_d6).is_ok());
925
926 let config_d7 = ValidationConfig {
928 draft: SchemaDraft::Draft7,
929 ..Default::default()
930 };
931 assert!(CompiledSchema::compile(&schema, &config_d7).is_ok());
932
933 let config_d201909 = ValidationConfig {
935 draft: SchemaDraft::Draft201909,
936 ..Default::default()
937 };
938 assert!(CompiledSchema::compile(&schema, &config_d201909).is_ok());
939
940 let config_d202012 = ValidationConfig {
942 draft: SchemaDraft::Draft202012,
943 ..Default::default()
944 };
945 assert!(CompiledSchema::compile(&schema, &config_d202012).is_ok());
946 }
947}