Skip to main content

fastmcp_protocol/
schema.rs

1//! JSON Schema validation for MCP tool inputs.
2//!
3//! This module provides a simple JSON Schema validator that covers the core
4//! requirements for MCP tool input validation:
5//!
6//! - Type checking (string, number, integer, boolean, object, array, null)
7//! - Required field validation
8//! - Enum validation
9//! - Property validation for objects
10//! - Items validation for arrays
11//!
12//! This is not a full JSON Schema implementation but covers the subset used by MCP.
13
14use serde_json::Value;
15use std::fmt;
16
17/// Error returned when JSON Schema validation fails.
18#[derive(Debug, Clone)]
19pub struct ValidationError {
20    /// Path to the invalid value (e.g., `root.foo.bar` or `root[0]`).
21    pub path: String,
22    /// Description of what went wrong.
23    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
34/// Result of JSON Schema validation.
35pub type ValidationResult = Result<(), Vec<ValidationError>>;
36
37/// Validates a JSON value against a JSON Schema.
38///
39/// # Arguments
40///
41/// * `schema` - The JSON Schema to validate against
42/// * `value` - The value to validate
43///
44/// # Returns
45///
46/// `Ok(())` if the value is valid, or `Err(Vec<ValidationError>)` with all
47/// validation errors found.
48///
49/// # Example
50///
51/// ```
52/// use fastmcp_protocol::schema::validate;
53/// use serde_json::json;
54///
55/// let schema = json!({
56///     "type": "object",
57///     "properties": {
58///         "name": { "type": "string" },
59///         "age": { "type": "integer" }
60///     },
61///     "required": ["name"]
62/// });
63///
64/// let valid = json!({ "name": "Alice", "age": 30 });
65/// assert!(validate(&schema, &valid).is_ok());
66///
67/// let invalid = json!({ "age": 30 });
68/// assert!(validate(&schema, &invalid).is_err());
69/// ```
70pub 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
81/// Validates a JSON value against a JSON Schema in strict mode.
82///
83/// Strict mode enforces `additionalProperties: false` on all object schemas,
84/// rejecting any properties not explicitly defined in the schema.
85///
86/// # Arguments
87///
88/// * `schema` - The JSON Schema to validate against
89/// * `value` - The value to validate
90///
91/// # Returns
92///
93/// `Ok(())` if the value is valid, or `Err(Vec<ValidationError>)` with all
94/// validation errors found.
95///
96/// # Example
97///
98/// ```
99/// use fastmcp_protocol::schema::validate_strict;
100/// use serde_json::json;
101///
102/// let schema = json!({
103///     "type": "object",
104///     "properties": {
105///         "name": { "type": "string" }
106///     }
107/// });
108///
109/// // Extra property "age" is rejected in strict mode
110/// let with_extra = json!({ "name": "Alice", "age": 30 });
111/// assert!(validate_strict(&schema, &with_extra).is_err());
112///
113/// // Only defined properties pass
114/// let valid = json!({ "name": "Alice" });
115/// assert!(validate_strict(&schema, &valid).is_ok());
116/// ```
117pub fn validate_strict(schema: &Value, value: &Value) -> ValidationResult {
118    // Clone and modify the schema to enforce additionalProperties: false
119    let strict_schema = make_strict_schema(schema);
120    validate(&strict_schema, value)
121}
122
123/// Recursively adds `additionalProperties: false` to all object schemas.
124fn make_strict_schema(schema: &Value) -> Value {
125    match schema {
126        Value::Object(obj) => {
127            let mut new_obj = obj.clone();
128
129            // Add additionalProperties: false if this is an object type schema
130            // and doesn't already have additionalProperties defined
131            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            // Recursively process nested schemas
143            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            // Handle additionalProperties if it's a schema object
152            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            // Handle items schema for arrays
162            if let Some(items) = obj.get("items") {
163                new_obj.insert("items".to_string(), make_strict_schema(items));
164            }
165
166            // Handle prefixItems for tuple validation
167            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            // Handle array schemas (union types in older drafts)
176            Value::Array(arr.iter().map(make_strict_schema).collect())
177        }
178        _ => schema.clone(),
179    }
180}
181
182/// Internal recursive validation function.
183fn validate_internal(schema: &Value, value: &Value, path: &str, errors: &mut Vec<ValidationError>) {
184    // Handle boolean schemas (true = accept all, false = reject all)
185    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    // Schema must be an object
196    let Some(schema_obj) = schema.as_object() else {
197        return; // Invalid schema, skip validation
198    };
199
200    // Check type constraint
201    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; // Type mismatch, skip further validation
213        }
214    }
215
216    // Check enum constraint
217    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    // Check const constraint
229    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    // Type-specific validation
239    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
256/// Validates type constraint.
257fn 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, // Invalid type constraint, skip
265    }
266}
267
268/// Checks if a value matches a single type name.
269fn 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, // Unknown type, accept
279    }
280}
281
282/// Returns the JSON type name for a value.
283fn 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
300/// Validates object-specific constraints.
301fn 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    // Check required fields
308    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    // Validate properties
322    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    // Check additionalProperties constraint
332    if let Some(additional) = schema.get("additionalProperties") {
333        // Get properties map directly - avoid collecting keys into Vec
334        let properties = schema.get("properties").and_then(|v| v.as_object());
335
336        for (key, value) in obj {
337            // Use contains_key directly on the Map (O(1) lookup) instead of Vec::contains (O(n))
338            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    // Check minProperties/maxProperties
358    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
382/// Validates array-specific constraints.
383fn validate_array(
384    schema: &serde_json::Map<String, Value>,
385    arr: &[Value],
386    path: &str,
387    errors: &mut Vec<ValidationError>,
388) {
389    // Validate prefixItems (tuple validation)
390    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    // Validate items (remaining items or all items)
402    if let Some(items_schema) = schema.get("items") {
403        // If items is an array (Draft 4-7 tuple), treat as prefixItems fallback if prefixItems absent
404        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                // In older drafts, 'additionalItems' controls the rest. We skip that for simplicity unless needed.
413            }
414        } else if items_schema.is_object() || items_schema.is_boolean() {
415            // Validate items starting from where prefixItems left off
416            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    // Check minItems/maxItems
424    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    // Check uniqueItems
442    if schema
443        .get("uniqueItems")
444        .and_then(serde_json::Value::as_bool)
445        .unwrap_or(false)
446    {
447        // Use HashSet with serialized JSON strings for O(1) lookup instead of O(n) Vec::contains
448        // This makes the overall algorithm O(n) instead of O(n²)
449        let mut seen = std::collections::HashSet::with_capacity(arr.len());
450        for (i, item) in arr.iter().enumerate() {
451            // Serialize to canonical JSON string for comparison
452            // serde_json produces consistent output for equal values
453            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
464/// Validates string-specific constraints.
465fn validate_string(
466    schema: &serde_json::Map<String, Value>,
467    s: &str,
468    path: &str,
469    errors: &mut Vec<ValidationError>,
470) {
471    // Check minLength/maxLength
472    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    // Check pattern (basic regex support could be added here)
491    // For now, we skip pattern validation to avoid regex dependency
492}
493
494/// Validates number-specific constraints.
495fn 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    // Check minimum/maximum
504    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    // Check exclusiveMinimum/exclusiveMaximum
522    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    // Check multipleOf
546    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        // true schema accepts everything
772        assert!(validate(&json!(true), &json!("anything")).is_ok());
773        assert!(validate(&json!(true), &json!(123)).is_ok());
774
775        // false schema rejects everything
776        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); // Missing both name and age
794    }
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    // ========================================================================
816    // Strict Validation Tests
817    // ========================================================================
818
819    #[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        // Regular validate allows extra properties
829        assert!(validate(&schema, &json!({"name": "Alice", "extra": 123})).is_ok());
830
831        // Strict validate rejects extra properties
832        assert!(validate_strict(&schema, &json!({"name": "Alice", "extra": 123})).is_err());
833
834        // Strict validate allows only defined properties
835        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        // Regular validate allows extra properties at any level
853        assert!(
854            validate(
855                &schema,
856                &json!({
857                    "person": {"name": "Alice", "age": 30}
858                })
859            )
860            .is_ok()
861        );
862
863        // Strict validate rejects extra properties at nested level
864        assert!(
865            validate_strict(
866                &schema,
867                &json!({
868                    "person": {"name": "Alice", "age": 30}
869                })
870            )
871            .is_err()
872        );
873
874        // Strict validate passes with only defined properties
875        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        // Schema explicitly allows additional properties with a specific type
889        let schema = json!({
890            "type": "object",
891            "properties": {
892                "name": {"type": "string"}
893            },
894            "additionalProperties": {"type": "integer"}
895        });
896
897        // With explicit additionalProperties schema, strict mode should honor it
898        assert!(
899            validate_strict(
900                &schema,
901                &json!({
902                    "name": "Alice",
903                    "count": 42
904                })
905            )
906            .is_ok()
907        );
908
909        // But still validate the type of additional properties
910        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        // Regular validate allows extra properties in array items
935        assert!(
936            validate(
937                &schema,
938                &json!([
939                    {"id": 1, "extra": "value"}
940                ])
941            )
942            .is_ok()
943        );
944
945        // Strict validate rejects extra properties in array items
946        assert!(
947            validate_strict(
948                &schema,
949                &json!([
950                    {"id": 1, "extra": "value"}
951                ])
952            )
953            .is_err()
954        );
955
956        // Strict validate passes with only defined properties
957        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        // Empty schema or true accepts everything
971        let schema = json!({});
972
973        // Empty schema doesn't have type: "object", so strict doesn't add additionalProperties
974        assert!(validate_strict(&schema, &json!({"anything": "goes"})).is_ok());
975    }
976
977    #[test]
978    fn test_validate_strict_non_object_types() {
979        // Strict mode shouldn't affect non-object types
980        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}