Skip to main content

jsonschema_schema/
validate.rs

1use crate::schema::{Schema, SchemaValue, navigate_pointer};
2
3/// A structural validation error found in a schema.
4#[derive(Debug, Clone)]
5pub struct SchemaError {
6    /// JSON Pointer to the error location (e.g. "/properties/item").
7    pub path: String,
8    /// Human-readable description of the problem.
9    pub message: String,
10}
11
12/// Validate structural integrity of a schema, returning all errors found.
13pub fn validate(schema: &Schema) -> Vec<SchemaError> {
14    let root = SchemaValue::Schema(Box::new(schema.clone()));
15    let mut errors = Vec::new();
16    validate_schema(schema, &root, "", &mut errors);
17    errors
18}
19
20fn validate_schema(schema: &Schema, root: &SchemaValue, path: &str, errors: &mut Vec<SchemaError>) {
21    // Check $ref
22    if let Some(ref ref_str) = schema.ref_
23        && let Some(ref_path) = ref_str.strip_prefix("#/")
24        && navigate_pointer(root, root, ref_path).is_err()
25    {
26        errors.push(SchemaError {
27            path: path.to_string(),
28            message: format!("$ref \"{ref_str}\" does not resolve"),
29        });
30    }
31
32    // Map fields (non-optional IndexMap)
33    for (keyword, map) in [
34        ("properties", &schema.properties),
35        ("patternProperties", &schema.pattern_properties),
36        ("dependentSchemas", &schema.dependent_schemas),
37    ] {
38        for (key, sv) in map {
39            validate_value(sv, root, &format!("{path}/{keyword}/{key}"), errors);
40        }
41    }
42
43    // $defs uses BTreeMap
44    if let Some(ref defs) = schema.defs {
45        for (key, sv) in defs {
46            validate_value(sv, root, &format!("{path}/$defs/{key}"), errors);
47        }
48    }
49
50    // Array fields
51    for (keyword, arr) in [
52        ("allOf", schema.all_of.as_ref()),
53        ("anyOf", schema.any_of.as_ref()),
54        ("oneOf", schema.one_of.as_ref()),
55        ("prefixItems", schema.prefix_items.as_ref()),
56    ] {
57        if let Some(items) = arr {
58            for (i, sv) in items.iter().enumerate() {
59                validate_value(sv, root, &format!("{path}/{keyword}/{i}"), errors);
60            }
61        }
62    }
63
64    // Single fields
65    for (keyword, field) in [
66        ("items", schema.items.as_deref()),
67        ("contains", schema.contains.as_deref()),
68        (
69            "additionalProperties",
70            schema.additional_properties.as_deref(),
71        ),
72        ("propertyNames", schema.property_names.as_deref()),
73        (
74            "unevaluatedProperties",
75            schema.unevaluated_properties.as_deref(),
76        ),
77        ("unevaluatedItems", schema.unevaluated_items.as_deref()),
78        ("not", schema.not.as_deref()),
79        ("if", schema.if_.as_deref()),
80        ("then", schema.then_.as_deref()),
81        ("else", schema.else_.as_deref()),
82        ("contentSchema", schema.content_schema.as_deref()),
83    ] {
84        if let Some(sv) = field {
85            validate_value(sv, root, &format!("{path}/{keyword}"), errors);
86        }
87    }
88}
89
90fn validate_value(sv: &SchemaValue, root: &SchemaValue, path: &str, errors: &mut Vec<SchemaError>) {
91    if let Some(schema) = sv.as_schema() {
92        validate_schema(schema, root, path, errors);
93    }
94}
95
96#[cfg(test)]
97#[allow(clippy::unwrap_used)]
98mod tests {
99    use super::*;
100    use crate::schema::{SimpleType, TypeValue};
101    use alloc::collections::BTreeMap;
102    use indexmap::IndexMap;
103
104    #[test]
105    fn valid_schema_no_errors() {
106        let item_schema = SchemaValue::Schema(Box::new(Schema {
107            type_: Some(TypeValue::Single(SimpleType::String)),
108            ..Default::default()
109        }));
110        let mut defs = BTreeMap::new();
111        defs.insert("Item".into(), item_schema);
112
113        let ref_schema = SchemaValue::Schema(Box::new(Schema {
114            ref_: Some("#/$defs/Item".into()),
115            ..Default::default()
116        }));
117        let mut props = IndexMap::new();
118        props.insert("item".into(), ref_schema);
119
120        let schema = Schema {
121            defs: Some(defs),
122            properties: props,
123            ..Default::default()
124        };
125
126        let errors = validate(&schema);
127        assert!(errors.is_empty(), "expected no errors, got: {errors:?}");
128    }
129
130    #[test]
131    fn missing_defs_target() {
132        let ref_schema = SchemaValue::Schema(Box::new(Schema {
133            ref_: Some("#/$defs/Missing".into()),
134            ..Default::default()
135        }));
136        let mut props = IndexMap::new();
137        props.insert("item".into(), ref_schema);
138
139        let schema = Schema {
140            properties: props,
141            ..Default::default()
142        };
143
144        let errors = validate(&schema);
145        assert_eq!(errors.len(), 1);
146        assert_eq!(errors[0].path, "/properties/item");
147        assert!(errors[0].message.contains("$defs/Missing"));
148    }
149
150    #[test]
151    fn nested_ref_in_properties() {
152        let ref_schema = SchemaValue::Schema(Box::new(Schema {
153            ref_: Some("#/$defs/Nonexistent".into()),
154            ..Default::default()
155        }));
156        let mut inner_props = IndexMap::new();
157        inner_props.insert("nested".into(), ref_schema);
158
159        let wrapper = SchemaValue::Schema(Box::new(Schema {
160            properties: inner_props,
161            ..Default::default()
162        }));
163        let mut props = IndexMap::new();
164        props.insert("wrapper".into(), wrapper);
165
166        let schema = Schema {
167            properties: props,
168            ..Default::default()
169        };
170
171        let errors = validate(&schema);
172        assert_eq!(errors.len(), 1);
173        assert_eq!(errors[0].path, "/properties/wrapper/properties/nested");
174    }
175
176    #[test]
177    fn external_ref_not_checked() {
178        let ref_schema = SchemaValue::Schema(Box::new(Schema {
179            ref_: Some("https://example.com/schema.json".into()),
180            ..Default::default()
181        }));
182        let mut props = IndexMap::new();
183        props.insert("item".into(), ref_schema);
184
185        let schema = Schema {
186            properties: props,
187            ..Default::default()
188        };
189
190        let errors = validate(&schema);
191        assert!(errors.is_empty(), "external $ref should not be checked");
192    }
193
194    #[test]
195    fn ref_in_all_of() {
196        let ref_schema = SchemaValue::Schema(Box::new(Schema {
197            ref_: Some("#/$defs/Missing".into()),
198            ..Default::default()
199        }));
200
201        let schema = Schema {
202            all_of: Some(vec![ref_schema]),
203            ..Default::default()
204        };
205
206        let errors = validate(&schema);
207        assert_eq!(errors.len(), 1);
208        assert_eq!(errors[0].path, "/allOf/0");
209    }
210
211    #[test]
212    fn ref_in_any_of() {
213        let ref_schema = SchemaValue::Schema(Box::new(Schema {
214            ref_: Some("#/$defs/Missing".into()),
215            ..Default::default()
216        }));
217
218        let schema = Schema {
219            any_of: Some(vec![SchemaValue::Bool(true), ref_schema]),
220            ..Default::default()
221        };
222
223        let errors = validate(&schema);
224        assert_eq!(errors.len(), 1);
225        assert_eq!(errors[0].path, "/anyOf/1");
226    }
227
228    #[test]
229    fn ref_in_one_of() {
230        let ref_schema = SchemaValue::Schema(Box::new(Schema {
231            ref_: Some("#/$defs/Also Missing".into()),
232            ..Default::default()
233        }));
234
235        let schema = Schema {
236            one_of: Some(vec![ref_schema]),
237            ..Default::default()
238        };
239
240        let errors = validate(&schema);
241        assert_eq!(errors.len(), 1);
242        assert_eq!(errors[0].path, "/oneOf/0");
243    }
244
245    #[test]
246    fn deep_nesting_full_path() {
247        let ref_schema = SchemaValue::Schema(Box::new(Schema {
248            ref_: Some("#/$defs/Deep".into()),
249            ..Default::default()
250        }));
251
252        let inner = SchemaValue::Schema(Box::new(Schema {
253            items: Some(Box::new(ref_schema)),
254            ..Default::default()
255        }));
256
257        let schema = Schema {
258            all_of: Some(vec![inner]),
259            ..Default::default()
260        };
261
262        let errors = validate(&schema);
263        assert_eq!(errors.len(), 1);
264        assert_eq!(errors[0].path, "/allOf/0/items");
265    }
266
267    #[test]
268    fn multiple_errors_collected() {
269        let ref1 = SchemaValue::Schema(Box::new(Schema {
270            ref_: Some("#/$defs/A".into()),
271            ..Default::default()
272        }));
273        let ref2 = SchemaValue::Schema(Box::new(Schema {
274            ref_: Some("#/$defs/B".into()),
275            ..Default::default()
276        }));
277        let mut props = IndexMap::new();
278        props.insert("x".into(), ref1);
279        props.insert("y".into(), ref2);
280
281        let schema = Schema {
282            properties: props,
283            ..Default::default()
284        };
285
286        let errors = validate(&schema);
287        assert_eq!(errors.len(), 2);
288    }
289
290    #[test]
291    fn validate_method_on_schema() {
292        let schema = Schema {
293            all_of: Some(vec![SchemaValue::Schema(Box::new(Schema {
294                ref_: Some("#/$defs/Nope".into()),
295                ..Default::default()
296            }))]),
297            ..Default::default()
298        };
299
300        let errors = schema.validate();
301        assert_eq!(errors.len(), 1);
302    }
303}