1use serde_json::Value;
15use std::fmt;
16
17#[derive(Debug, Clone)]
19pub struct ValidationError {
20 pub path: String,
22 pub message: String,
24}
25
26impl fmt::Display for ValidationError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 write!(f, "{}: {}", self.path, self.message)
29 }
30}
31
32impl std::error::Error for ValidationError {}
33
34pub type ValidationResult = Result<(), Vec<ValidationError>>;
36
37pub fn validate(schema: &Value, value: &Value) -> ValidationResult {
71 let mut errors = Vec::new();
72 validate_internal(schema, value, "root", &mut errors);
73
74 if errors.is_empty() {
75 Ok(())
76 } else {
77 Err(errors)
78 }
79}
80
81pub fn validate_strict(schema: &Value, value: &Value) -> ValidationResult {
118 let strict_schema = make_strict_schema(schema);
120 validate(&strict_schema, value)
121}
122
123fn make_strict_schema(schema: &Value) -> Value {
125 match schema {
126 Value::Object(obj) => {
127 let mut new_obj = obj.clone();
128
129 if let Some(type_val) = obj.get("type") {
132 let is_object_type = type_val == "object"
133 || type_val
134 .as_array()
135 .is_some_and(|arr| arr.iter().any(|t| t == "object"));
136
137 if is_object_type && !obj.contains_key("additionalProperties") {
138 new_obj.insert("additionalProperties".to_string(), Value::Bool(false));
139 }
140 }
141
142 if let Some(Value::Object(props)) = obj.get("properties") {
144 let strict_props: serde_json::Map<String, Value> = props
145 .iter()
146 .map(|(k, v)| (k.clone(), make_strict_schema(v)))
147 .collect();
148 new_obj.insert("properties".to_string(), Value::Object(strict_props));
149 }
150
151 if let Some(additional) = obj.get("additionalProperties") {
153 if additional.is_object() {
154 new_obj.insert(
155 "additionalProperties".to_string(),
156 make_strict_schema(additional),
157 );
158 }
159 }
160
161 if let Some(items) = obj.get("items") {
163 new_obj.insert("items".to_string(), make_strict_schema(items));
164 }
165
166 if let Some(Value::Array(arr)) = obj.get("prefixItems") {
168 let strict_items: Vec<Value> = arr.iter().map(make_strict_schema).collect();
169 new_obj.insert("prefixItems".to_string(), Value::Array(strict_items));
170 }
171
172 Value::Object(new_obj)
173 }
174 Value::Array(arr) => {
175 Value::Array(arr.iter().map(make_strict_schema).collect())
177 }
178 _ => schema.clone(),
179 }
180}
181
182fn validate_internal(schema: &Value, value: &Value, path: &str, errors: &mut Vec<ValidationError>) {
184 if let Some(b) = schema.as_bool() {
186 if !b {
187 errors.push(ValidationError {
188 path: path.to_string(),
189 message: "schema rejects all values".to_string(),
190 });
191 }
192 return;
193 }
194
195 let Some(schema_obj) = schema.as_object() else {
197 return; };
199
200 if let Some(type_val) = schema_obj.get("type") {
202 if !validate_type(type_val, value) {
203 let expected = type_val
204 .as_str()
205 .map(String::from)
206 .or_else(|| type_val.as_array().map(|arr| format!("{arr:?}")))
207 .unwrap_or_else(|| "unknown".to_string());
208 errors.push(ValidationError {
209 path: path.to_string(),
210 message: format!("expected type {expected}, got {}", json_type_name(value)),
211 });
212 return; }
214 }
215
216 if let Some(enum_val) = schema_obj.get("enum") {
218 if let Some(enum_arr) = enum_val.as_array() {
219 if !enum_arr.contains(value) {
220 errors.push(ValidationError {
221 path: path.to_string(),
222 message: format!("value must be one of: {enum_arr:?}"),
223 });
224 }
225 }
226 }
227
228 if let Some(const_val) = schema_obj.get("const") {
230 if value != const_val {
231 errors.push(ValidationError {
232 path: path.to_string(),
233 message: format!("value must equal {const_val}"),
234 });
235 }
236 }
237
238 match value {
240 Value::Object(obj) => {
241 validate_object(schema_obj, obj, path, errors);
242 }
243 Value::Array(arr) => {
244 validate_array(schema_obj, arr, path, errors);
245 }
246 Value::String(s) => {
247 validate_string(schema_obj, s, path, errors);
248 }
249 Value::Number(n) => {
250 validate_number(schema_obj, n, path, errors);
251 }
252 _ => {}
253 }
254}
255
256fn validate_type(type_val: &Value, value: &Value) -> bool {
258 match type_val {
259 Value::String(t) => matches_type(t, value),
260 Value::Array(types) => types.iter().any(|t| {
261 t.as_str()
262 .is_some_and(|type_str| matches_type(type_str, value))
263 }),
264 _ => true, }
266}
267
268fn matches_type(type_name: &str, value: &Value) -> bool {
270 match type_name {
271 "string" => value.is_string(),
272 "number" => value.is_number(),
273 "integer" => value.is_i64() || value.is_u64(),
274 "boolean" => value.is_boolean(),
275 "object" => value.is_object(),
276 "array" => value.is_array(),
277 "null" => value.is_null(),
278 _ => true, }
280}
281
282fn json_type_name(value: &Value) -> &'static str {
284 match value {
285 Value::Null => "null",
286 Value::Bool(_) => "boolean",
287 Value::Number(n) => {
288 if n.is_i64() || n.is_u64() {
289 "integer"
290 } else {
291 "number"
292 }
293 }
294 Value::String(_) => "string",
295 Value::Array(_) => "array",
296 Value::Object(_) => "object",
297 }
298}
299
300fn validate_object(
302 schema: &serde_json::Map<String, Value>,
303 obj: &serde_json::Map<String, Value>,
304 path: &str,
305 errors: &mut Vec<ValidationError>,
306) {
307 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
309 for req in required {
310 if let Some(req_name) = req.as_str() {
311 if !obj.contains_key(req_name) {
312 errors.push(ValidationError {
313 path: path.to_string(),
314 message: format!("missing required field: {req_name}"),
315 });
316 }
317 }
318 }
319 }
320
321 if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
323 for (key, value) in obj {
324 if let Some(prop_schema) = properties.get(key) {
325 let prop_path = format!("{path}.{key}");
326 validate_internal(prop_schema, value, &prop_path, errors);
327 }
328 }
329 }
330
331 if let Some(additional) = schema.get("additionalProperties") {
333 let properties = schema.get("properties").and_then(|v| v.as_object());
335
336 for (key, value) in obj {
337 let is_defined_property = properties.is_some_and(|p| p.contains_key(key));
339 if !is_defined_property {
340 match additional {
341 Value::Bool(false) => {
342 errors.push(ValidationError {
343 path: path.to_string(),
344 message: format!("additional property not allowed: {key}"),
345 });
346 }
347 Value::Object(_) => {
348 let prop_path = format!("{path}.{key}");
349 validate_internal(additional, value, &prop_path, errors);
350 }
351 _ => {}
352 }
353 }
354 }
355 }
356
357 if let Some(min) = schema
359 .get("minProperties")
360 .and_then(serde_json::Value::as_u64)
361 {
362 if (obj.len() as u64) < min {
363 errors.push(ValidationError {
364 path: path.to_string(),
365 message: format!("object must have at least {min} properties"),
366 });
367 }
368 }
369 if let Some(max) = schema
370 .get("maxProperties")
371 .and_then(serde_json::Value::as_u64)
372 {
373 if (obj.len() as u64) > max {
374 errors.push(ValidationError {
375 path: path.to_string(),
376 message: format!("object must have at most {max} properties"),
377 });
378 }
379 }
380}
381
382fn validate_array(
384 schema: &serde_json::Map<String, Value>,
385 arr: &[Value],
386 path: &str,
387 errors: &mut Vec<ValidationError>,
388) {
389 let mut prefix_len = 0;
391 if let Some(prefix_items) = schema.get("prefixItems").and_then(|v| v.as_array()) {
392 prefix_len = prefix_items.len();
393 for (i, item_schema) in prefix_items.iter().enumerate() {
394 if let Some(item) = arr.get(i) {
395 let item_path = format!("{path}[{i}]");
396 validate_internal(item_schema, item, &item_path, errors);
397 }
398 }
399 }
400
401 if let Some(items_schema) = schema.get("items") {
403 if items_schema.is_array() && prefix_len == 0 {
405 if let Some(items_arr) = items_schema.as_array() {
406 for (i, item_schema) in items_arr.iter().enumerate() {
407 if let Some(item) = arr.get(i) {
408 let item_path = format!("{path}[{i}]");
409 validate_internal(item_schema, item, &item_path, errors);
410 }
411 }
412 }
414 } else if items_schema.is_object() || items_schema.is_boolean() {
415 for (i, item) in arr.iter().enumerate().skip(prefix_len) {
417 let item_path = format!("{path}[{i}]");
418 validate_internal(items_schema, item, &item_path, errors);
419 }
420 }
421 }
422
423 if let Some(min) = schema.get("minItems").and_then(serde_json::Value::as_u64) {
425 if (arr.len() as u64) < min {
426 errors.push(ValidationError {
427 path: path.to_string(),
428 message: format!("array must have at least {min} items"),
429 });
430 }
431 }
432 if let Some(max) = schema.get("maxItems").and_then(serde_json::Value::as_u64) {
433 if (arr.len() as u64) > max {
434 errors.push(ValidationError {
435 path: path.to_string(),
436 message: format!("array must have at most {max} items"),
437 });
438 }
439 }
440
441 if schema
443 .get("uniqueItems")
444 .and_then(serde_json::Value::as_bool)
445 .unwrap_or(false)
446 {
447 let mut seen = std::collections::HashSet::with_capacity(arr.len());
450 for (i, item) in arr.iter().enumerate() {
451 let key = serde_json::to_string(item).unwrap_or_default();
454 if !seen.insert(key) {
455 errors.push(ValidationError {
456 path: format!("{path}[{i}]"),
457 message: "duplicate item in array".to_string(),
458 });
459 }
460 }
461 }
462}
463
464fn validate_string(
466 schema: &serde_json::Map<String, Value>,
467 s: &str,
468 path: &str,
469 errors: &mut Vec<ValidationError>,
470) {
471 let len = s.chars().count();
473 if let Some(min) = schema.get("minLength").and_then(serde_json::Value::as_u64) {
474 if (len as u64) < min {
475 errors.push(ValidationError {
476 path: path.to_string(),
477 message: format!("string must be at least {min} characters"),
478 });
479 }
480 }
481 if let Some(max) = schema.get("maxLength").and_then(serde_json::Value::as_u64) {
482 if (len as u64) > max {
483 errors.push(ValidationError {
484 path: path.to_string(),
485 message: format!("string must be at most {max} characters"),
486 });
487 }
488 }
489
490 }
493
494fn validate_number(
496 schema: &serde_json::Map<String, Value>,
497 n: &serde_json::Number,
498 path: &str,
499 errors: &mut Vec<ValidationError>,
500) {
501 let val = n.as_f64().unwrap_or(0.0);
502
503 if let Some(min) = schema.get("minimum").and_then(serde_json::Value::as_f64) {
505 if val < min {
506 errors.push(ValidationError {
507 path: path.to_string(),
508 message: format!("value must be >= {min}"),
509 });
510 }
511 }
512 if let Some(max) = schema.get("maximum").and_then(serde_json::Value::as_f64) {
513 if val > max {
514 errors.push(ValidationError {
515 path: path.to_string(),
516 message: format!("value must be <= {max}"),
517 });
518 }
519 }
520
521 if let Some(min) = schema
523 .get("exclusiveMinimum")
524 .and_then(serde_json::Value::as_f64)
525 {
526 if val <= min {
527 errors.push(ValidationError {
528 path: path.to_string(),
529 message: format!("value must be > {min}"),
530 });
531 }
532 }
533 if let Some(max) = schema
534 .get("exclusiveMaximum")
535 .and_then(serde_json::Value::as_f64)
536 {
537 if val >= max {
538 errors.push(ValidationError {
539 path: path.to_string(),
540 message: format!("value must be < {max}"),
541 });
542 }
543 }
544
545 if let Some(multiple) = schema.get("multipleOf").and_then(serde_json::Value::as_f64) {
547 if multiple != 0.0 && (val % multiple).abs() > f64::EPSILON {
548 errors.push(ValidationError {
549 path: path.to_string(),
550 message: format!("value must be a multiple of {multiple}"),
551 });
552 }
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use serde_json::json;
560
561 #[test]
562 fn test_type_validation_string() {
563 let schema = json!({"type": "string"});
564 assert!(validate(&schema, &json!("hello")).is_ok());
565 assert!(validate(&schema, &json!(123)).is_err());
566 }
567
568 #[test]
569 fn test_type_validation_number() {
570 let schema = json!({"type": "number"});
571 assert!(validate(&schema, &json!(123)).is_ok());
572 assert!(validate(&schema, &json!(12.5)).is_ok());
573 assert!(validate(&schema, &json!("hello")).is_err());
574 }
575
576 #[test]
577 fn test_type_validation_integer() {
578 let schema = json!({"type": "integer"});
579 assert!(validate(&schema, &json!(123)).is_ok());
580 assert!(validate(&schema, &json!(12.5)).is_err());
581 }
582
583 #[test]
584 fn test_type_validation_boolean() {
585 let schema = json!({"type": "boolean"});
586 assert!(validate(&schema, &json!(true)).is_ok());
587 assert!(validate(&schema, &json!(false)).is_ok());
588 assert!(validate(&schema, &json!(1)).is_err());
589 }
590
591 #[test]
592 fn test_type_validation_object() {
593 let schema = json!({"type": "object"});
594 assert!(validate(&schema, &json!({})).is_ok());
595 assert!(validate(&schema, &json!({"a": 1})).is_ok());
596 assert!(validate(&schema, &json!([])).is_err());
597 }
598
599 #[test]
600 fn test_type_validation_array() {
601 let schema = json!({"type": "array"});
602 assert!(validate(&schema, &json!([])).is_ok());
603 assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
604 assert!(validate(&schema, &json!({})).is_err());
605 }
606
607 #[test]
608 fn test_type_validation_null() {
609 let schema = json!({"type": "null"});
610 assert!(validate(&schema, &json!(null)).is_ok());
611 assert!(validate(&schema, &json!(0)).is_err());
612 }
613
614 #[test]
615 fn test_type_validation_union() {
616 let schema = json!({"type": ["string", "number"]});
617 assert!(validate(&schema, &json!("hello")).is_ok());
618 assert!(validate(&schema, &json!(123)).is_ok());
619 assert!(validate(&schema, &json!(true)).is_err());
620 }
621
622 #[test]
623 fn test_required_fields() {
624 let schema = json!({
625 "type": "object",
626 "properties": {
627 "name": {"type": "string"},
628 "age": {"type": "integer"}
629 },
630 "required": ["name"]
631 });
632
633 assert!(validate(&schema, &json!({"name": "Alice"})).is_ok());
634 assert!(validate(&schema, &json!({"name": "Alice", "age": 30})).is_ok());
635 assert!(validate(&schema, &json!({"age": 30})).is_err());
636 assert!(validate(&schema, &json!({})).is_err());
637 }
638
639 #[test]
640 fn test_enum_validation() {
641 let schema = json!({"enum": ["red", "green", "blue"]});
642 assert!(validate(&schema, &json!("red")).is_ok());
643 assert!(validate(&schema, &json!("yellow")).is_err());
644 }
645
646 #[test]
647 fn test_const_validation() {
648 let schema = json!({"const": "fixed"});
649 assert!(validate(&schema, &json!("fixed")).is_ok());
650 assert!(validate(&schema, &json!("other")).is_err());
651 }
652
653 #[test]
654 fn test_string_length() {
655 let schema = json!({
656 "type": "string",
657 "minLength": 2,
658 "maxLength": 5
659 });
660
661 assert!(validate(&schema, &json!("ab")).is_ok());
662 assert!(validate(&schema, &json!("abcde")).is_ok());
663 assert!(validate(&schema, &json!("a")).is_err());
664 assert!(validate(&schema, &json!("abcdef")).is_err());
665 }
666
667 #[test]
668 fn test_number_range() {
669 let schema = json!({
670 "type": "number",
671 "minimum": 0,
672 "maximum": 100
673 });
674
675 assert!(validate(&schema, &json!(0)).is_ok());
676 assert!(validate(&schema, &json!(50)).is_ok());
677 assert!(validate(&schema, &json!(100)).is_ok());
678 assert!(validate(&schema, &json!(-1)).is_err());
679 assert!(validate(&schema, &json!(101)).is_err());
680 }
681
682 #[test]
683 fn test_number_exclusive_range() {
684 let schema = json!({
685 "type": "number",
686 "exclusiveMinimum": 0,
687 "exclusiveMaximum": 10
688 });
689
690 assert!(validate(&schema, &json!(1)).is_ok());
691 assert!(validate(&schema, &json!(9)).is_ok());
692 assert!(validate(&schema, &json!(0)).is_err());
693 assert!(validate(&schema, &json!(10)).is_err());
694 }
695
696 #[test]
697 fn test_array_items() {
698 let schema = json!({
699 "type": "array",
700 "items": {"type": "integer"}
701 });
702
703 assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
704 assert!(validate(&schema, &json!([])).is_ok());
705 assert!(validate(&schema, &json!([1, "two", 3])).is_err());
706 }
707
708 #[test]
709 fn test_array_length() {
710 let schema = json!({
711 "type": "array",
712 "minItems": 1,
713 "maxItems": 3
714 });
715
716 assert!(validate(&schema, &json!([1])).is_ok());
717 assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
718 assert!(validate(&schema, &json!([])).is_err());
719 assert!(validate(&schema, &json!([1, 2, 3, 4])).is_err());
720 }
721
722 #[test]
723 fn test_unique_items() {
724 let schema = json!({
725 "type": "array",
726 "uniqueItems": true
727 });
728
729 assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
730 assert!(validate(&schema, &json!([1, 1, 2])).is_err());
731 }
732
733 #[test]
734 fn test_nested_object() {
735 let schema = json!({
736 "type": "object",
737 "properties": {
738 "person": {
739 "type": "object",
740 "properties": {
741 "name": {"type": "string"},
742 "age": {"type": "integer"}
743 },
744 "required": ["name"]
745 }
746 }
747 });
748
749 assert!(validate(&schema, &json!({"person": {"name": "Alice"}})).is_ok());
750 assert!(validate(&schema, &json!({"person": {"name": "Alice", "age": 30}})).is_ok());
751 assert!(validate(&schema, &json!({"person": {"age": 30}})).is_err());
752 }
753
754 #[test]
755 fn test_additional_properties_false() {
756 let schema = json!({
757 "type": "object",
758 "properties": {
759 "name": {"type": "string"}
760 },
761 "additionalProperties": false
762 });
763
764 assert!(validate(&schema, &json!({"name": "Alice"})).is_ok());
765 assert!(validate(&schema, &json!({})).is_ok());
766 assert!(validate(&schema, &json!({"name": "Alice", "extra": 1})).is_err());
767 }
768
769 #[test]
770 fn test_boolean_schema() {
771 assert!(validate(&json!(true), &json!("anything")).is_ok());
773 assert!(validate(&json!(true), &json!(123)).is_ok());
774
775 assert!(validate(&json!(false), &json!("anything")).is_err());
777 }
778
779 #[test]
780 fn test_multiple_errors() {
781 let schema = json!({
782 "type": "object",
783 "properties": {
784 "name": {"type": "string"},
785 "age": {"type": "integer"}
786 },
787 "required": ["name", "age"]
788 });
789
790 let result = validate(&schema, &json!({}));
791 assert!(result.is_err());
792 let errors = result.unwrap_err();
793 assert_eq!(errors.len(), 2); }
795
796 #[test]
797 fn test_error_path() {
798 let schema = json!({
799 "type": "object",
800 "properties": {
801 "items": {
802 "type": "array",
803 "items": {"type": "integer"}
804 }
805 }
806 });
807
808 let result = validate(&schema, &json!({"items": [1, "two", 3]}));
809 assert!(result.is_err());
810 let errors = result.unwrap_err();
811 assert_eq!(errors.len(), 1);
812 assert_eq!(errors[0].path, "root.items[1]");
813 }
814
815 #[test]
820 fn test_validate_strict_rejects_extra_properties() {
821 let schema = json!({
822 "type": "object",
823 "properties": {
824 "name": {"type": "string"}
825 }
826 });
827
828 assert!(validate(&schema, &json!({"name": "Alice", "extra": 123})).is_ok());
830
831 assert!(validate_strict(&schema, &json!({"name": "Alice", "extra": 123})).is_err());
833
834 assert!(validate_strict(&schema, &json!({"name": "Alice"})).is_ok());
836 }
837
838 #[test]
839 fn test_validate_strict_nested_objects() {
840 let schema = json!({
841 "type": "object",
842 "properties": {
843 "person": {
844 "type": "object",
845 "properties": {
846 "name": {"type": "string"}
847 }
848 }
849 }
850 });
851
852 assert!(
854 validate(
855 &schema,
856 &json!({
857 "person": {"name": "Alice", "age": 30}
858 })
859 )
860 .is_ok()
861 );
862
863 assert!(
865 validate_strict(
866 &schema,
867 &json!({
868 "person": {"name": "Alice", "age": 30}
869 })
870 )
871 .is_err()
872 );
873
874 assert!(
876 validate_strict(
877 &schema,
878 &json!({
879 "person": {"name": "Alice"}
880 })
881 )
882 .is_ok()
883 );
884 }
885
886 #[test]
887 fn test_validate_strict_preserves_explicit_additional_properties() {
888 let schema = json!({
890 "type": "object",
891 "properties": {
892 "name": {"type": "string"}
893 },
894 "additionalProperties": {"type": "integer"}
895 });
896
897 assert!(
899 validate_strict(
900 &schema,
901 &json!({
902 "name": "Alice",
903 "count": 42
904 })
905 )
906 .is_ok()
907 );
908
909 assert!(
911 validate_strict(
912 &schema,
913 &json!({
914 "name": "Alice",
915 "count": "not an integer"
916 })
917 )
918 .is_err()
919 );
920 }
921
922 #[test]
923 fn test_validate_strict_array_items() {
924 let schema = json!({
925 "type": "array",
926 "items": {
927 "type": "object",
928 "properties": {
929 "id": {"type": "integer"}
930 }
931 }
932 });
933
934 assert!(
936 validate(
937 &schema,
938 &json!([
939 {"id": 1, "extra": "value"}
940 ])
941 )
942 .is_ok()
943 );
944
945 assert!(
947 validate_strict(
948 &schema,
949 &json!([
950 {"id": 1, "extra": "value"}
951 ])
952 )
953 .is_err()
954 );
955
956 assert!(
958 validate_strict(
959 &schema,
960 &json!([
961 {"id": 1}
962 ])
963 )
964 .is_ok()
965 );
966 }
967
968 #[test]
969 fn test_validate_strict_empty_schema() {
970 let schema = json!({});
972
973 assert!(validate_strict(&schema, &json!({"anything": "goes"})).is_ok());
975 }
976
977 #[test]
978 fn test_validate_strict_non_object_types() {
979 let string_schema = json!({"type": "string"});
981 assert!(validate_strict(&string_schema, &json!("hello")).is_ok());
982
983 let number_schema = json!({"type": "number"});
984 assert!(validate_strict(&number_schema, &json!(42)).is_ok());
985
986 let array_schema = json!({"type": "array"});
987 assert!(validate_strict(&array_schema, &json!([1, 2, 3])).is_ok());
988 }
989}