1use crate::schema::{Schema, SchemaValue, navigate_pointer};
2
3#[derive(Debug, Clone)]
5pub struct SchemaError {
6 pub path: String,
8 pub message: String,
10}
11
12pub 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 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 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 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 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 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}