1use std::fmt;
47
48use serde_json::{Map, Number, Value};
49
50#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct Problem {
53 pub at: String,
56 pub what: String,
58}
59
60impl fmt::Display for Problem {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 if self.at.is_empty() {
64 f.write_str(&self.what)
65 } else {
66 write!(f, "{}: {}", self.at, self.what)
67 }
68 }
69}
70
71pub fn validate(schema: &Value, value: &Value) -> Result<(), Vec<Problem>> {
75 let problems = Validator { root: schema }.check(schema, value, "", &[]);
76 if problems.is_empty() {
77 Ok(())
78 } else {
79 Err(problems)
80 }
81}
82
83struct Validator<'s> {
85 root: &'s Value,
86}
87
88impl<'s> Validator<'s> {
89 fn check(&self, schema: &'s Value, value: &Value, at: &str, refs: &[&'s str]) -> Vec<Problem> {
93 let schema = match schema {
94 Value::Object(schema) => schema,
95 Value::Bool(false) => return vec![problem(at, "no value is allowed here")],
96 _ => return Vec::new(),
97 };
98 let mut problems = Vec::new();
99 if let Some(Value::String(reference)) = schema.get("$ref")
100 && !refs.contains(&reference.as_str())
101 && let Some(target) = self.resolve(reference)
102 {
103 let mut followed = refs.to_vec();
104 followed.push(reference);
105 problems.extend(self.check(target, value, at, &followed));
106 }
107 if let Some(types) = schema.get("type")
108 && !fits_type(types, value)
109 {
110 problems.push(problem(
111 at,
112 format!("expected {}, got {}", expected(types), kind(value)),
113 ));
114 return problems;
115 }
116 if let Some(Value::Array(allowed)) = schema.get("enum")
117 && !allowed.iter().any(|it| same(it, value))
118 {
119 let allowed: Vec<String> = allowed.iter().map(Value::to_string).collect();
120 let what = format!(
121 "expected one of {}, got {}",
122 allowed.join(", "),
123 shown(value)
124 );
125 problems.push(problem(at, what));
126 }
127 if let Some(constant) = schema.get("const")
128 && !same(constant, value)
129 {
130 problems.push(problem(
131 at,
132 format!("expected {constant}, got {}", shown(value)),
133 ));
134 }
135 match value {
136 Value::Object(fields) => problems.extend(self.object(schema, fields, at)),
137 Value::Array(items) => problems.extend(self.array(schema, items, at)),
138 Value::String(text) => {
139 let length = text.chars().count();
140 problems.extend(count(
141 schema,
142 "minLength",
143 "maxLength",
144 "character",
145 length,
146 at,
147 ));
148 }
149 Value::Number(number) => problems.extend(bounds(schema, number, at)),
150 Value::Null | Value::Bool(_) => {}
151 }
152 if let Some(Value::Array(all)) = schema.get("allOf") {
153 for alternative in all {
154 problems.extend(self.check(alternative, value, at, refs));
155 }
156 }
157 if let Some(Value::Array(any)) = schema.get("anyOf") {
158 let results: Vec<_> = any
159 .iter()
160 .map(|it| self.check(it, value, at, refs))
161 .collect();
162 if !results.iter().any(Vec::is_empty) {
163 problems.extend(closest(any, results, value, at, "anyOf"));
164 }
165 }
166 if let Some(Value::Array(one)) = schema.get("oneOf") {
167 let results: Vec<_> = one
168 .iter()
169 .map(|it| self.check(it, value, at, refs))
170 .collect();
171 match results.iter().filter(|it| it.is_empty()).count() {
172 0 => problems.extend(closest(one, results, value, at, "oneOf")),
173 1 => {}
174 fitting => problems.push(problem(
175 at,
176 format!("fits {fitting} of its `oneOf` alternatives, not exactly one"),
177 )),
178 }
179 }
180 problems
181 }
182
183 fn object(
188 &self,
189 schema: &'s Map<String, Value>,
190 fields: &Map<String, Value>,
191 at: &str,
192 ) -> Vec<Problem> {
193 let mut problems = Vec::new();
194 let properties = schema.get("properties").and_then(Value::as_object);
195 let additional = schema
196 .get("additionalProperties")
197 .filter(|_| !schema.contains_key("patternProperties"));
198 let required: Vec<&str> = match schema.get("required") {
199 Some(Value::Array(required)) => required.iter().filter_map(Value::as_str).collect(),
200 _ => Vec::new(),
201 };
202 for (key, field) in fields {
203 let at = child(at, key);
204 match (
205 properties.and_then(|declared| declared.get(key)),
206 additional,
207 ) {
208 (Some(_), _) if field.is_null() && !required.contains(&key.as_str()) => {}
209 (Some(property), _) => problems.extend(self.check(property, field, &at, &[])),
210 (None, Some(Value::Bool(false))) => {
211 let allowed: Vec<String> = properties
212 .into_iter()
213 .flat_map(Map::keys)
214 .map(|name| format!("`{name}`"))
215 .collect();
216 let what = if allowed.is_empty() {
217 "not allowed: no keys are".to_owned()
218 } else {
219 format!("not allowed; the keys are {}", allowed.join(", "))
220 };
221 problems.push(problem(&at, what));
222 }
223 (None, Some(additional)) => {
224 problems.extend(self.check(additional, field, &at, &[]))
225 }
226 (None, None) => {}
227 }
228 }
229 for name in required {
230 if !fields.contains_key(name) {
231 problems.push(problem(&child(at, name), "missing"));
232 }
233 }
234 problems
235 }
236
237 fn array(&self, schema: &'s Map<String, Value>, items: &[Value], at: &str) -> Vec<Problem> {
241 let mut problems = count(schema, "minItems", "maxItems", "item", items.len(), at);
242 let (prefix, rest) = match (schema.get("prefixItems"), schema.get("items")) {
243 (Some(Value::Array(prefix)), rest) => (prefix.as_slice(), rest),
244 (_, Some(Value::Array(prefix))) => (prefix.as_slice(), None),
245 (_, rest) => (&[][..], rest),
246 };
247 for (index, item) in items.iter().enumerate() {
248 let subschema = prefix
249 .get(index)
250 .or_else(|| rest.filter(|_| index >= prefix.len()));
251 if let Some(subschema) = subschema {
252 problems.extend(self.check(subschema, item, &child(at, &index.to_string()), &[]));
253 }
254 }
255 problems
256 }
257
258 fn resolve(&self, reference: &str) -> Option<&'s Value> {
260 let pointer = reference.strip_prefix('#')?;
261 if pointer.is_empty() {
262 Some(self.root)
263 } else if pointer.starts_with('/') {
264 self.root.pointer(pointer)
265 } else {
266 None
267 }
268 }
269}
270
271fn closest(
275 alternatives: &[Value],
276 results: Vec<Vec<Problem>>,
277 value: &Value,
278 at: &str,
279 keyword: &str,
280) -> Vec<Problem> {
281 let types: Option<Vec<&Value>> = alternatives
282 .iter()
283 .map(|alternative| {
284 alternative
285 .get("type")
286 .filter(|types| !fits_type(types, value))
287 })
288 .collect();
289 if let Some(types) = types.filter(|types| !types.is_empty()) {
290 let names: Vec<String> = types.into_iter().map(expected).collect();
291 return vec![problem(
292 at,
293 format!("expected {}, got {}", names.join(" or "), kind(value)),
294 )];
295 }
296 results
297 .into_iter()
298 .filter(|problems| problems.iter().all(|it| it.at.len() > at.len()))
299 .min_by_key(Vec::len)
300 .unwrap_or_else(|| {
301 vec![problem(
302 at,
303 format!("fits none of its `{keyword}` alternatives"),
304 )]
305 })
306}
307
308fn count(
311 schema: &Map<String, Value>,
312 min: &str,
313 max: &str,
314 unit: &str,
315 length: usize,
316 at: &str,
317) -> Vec<Problem> {
318 let limit = |key| schema.get(key).and_then(Value::as_u64);
319 let plural = |n: u64| {
320 if n == 1 {
321 format!("{n} {unit}")
322 } else {
323 format!("{n} {unit}s")
324 }
325 };
326 let length = u64::try_from(length).unwrap_or(u64::MAX);
327 let mut problems = Vec::new();
328 if let Some(min) = limit(min).filter(|min| length < *min) {
329 problems.push(problem(
330 at,
331 format!("expected at least {}, got {length}", plural(min)),
332 ));
333 }
334 if let Some(max) = limit(max).filter(|max| length > *max) {
335 problems.push(problem(
336 at,
337 format!("expected at most {}, got {length}", plural(max)),
338 ));
339 }
340 problems
341}
342
343fn bounds(schema: &Map<String, Value>, number: &Number, at: &str) -> Vec<Problem> {
345 let Some(n) = number.as_f64() else {
346 return Vec::new();
347 };
348 let checks: [(&str, &str, fn(f64, f64) -> bool); 4] = [
349 ("minimum", "at least", |n, bound| n >= bound),
350 ("maximum", "at most", |n, bound| n <= bound),
351 ("exclusiveMinimum", "more than", |n, bound| n > bound),
352 ("exclusiveMaximum", "less than", |n, bound| n < bound),
353 ];
354 checks
355 .into_iter()
356 .filter_map(|(key, words, fits)| {
357 let bound = schema.get(key).filter(|bound| bound.is_number())?;
358 let fit = bound.as_f64().is_none_or(|bound| fits(n, bound));
359 (!fit).then(|| problem(at, format!("expected {words} {bound}, got {number}")))
360 })
361 .collect()
362}
363
364fn fits_type(types: &Value, value: &Value) -> bool {
367 let is = |name: &str| match name {
368 "string" => value.is_string(),
369 "integer" => value.as_number().is_some_and(integral),
370 "number" => value.is_number(),
371 "boolean" => value.is_boolean(),
372 "array" => value.is_array(),
373 "object" => value.is_object(),
374 "null" => value.is_null(),
375 _ => true,
376 };
377 match types {
378 Value::String(name) => is(name),
379 Value::Array(names) => names.iter().filter_map(Value::as_str).any(is),
380 _ => true,
381 }
382}
383
384fn expected(types: &Value) -> String {
386 let word = |name: &str| match name {
387 "string" | "number" | "boolean" => format!("a {name}"),
388 "integer" | "array" | "object" => format!("an {name}"),
389 _ => name.to_owned(),
390 };
391 match types {
392 Value::String(name) => word(name),
393 Value::Array(names) => {
394 let words: Vec<String> = names.iter().filter_map(Value::as_str).map(word).collect();
395 words.join(" or ")
396 }
397 other => other.to_string(),
398 }
399}
400
401fn kind(value: &Value) -> &'static str {
403 match value {
404 Value::Null => "null",
405 Value::Bool(_) => "a boolean",
406 Value::Number(number) if integral(number) => "a number",
407 Value::Number(_) => "a number with a fraction",
408 Value::String(_) => "a string",
409 Value::Array(_) => "an array",
410 Value::Object(_) => "an object",
411 }
412}
413
414fn shown(value: &Value) -> String {
416 match value {
417 Value::Array(_) | Value::Object(_) => kind(value).to_owned(),
418 Value::String(text) if text.chars().count() > 40 => kind(value).to_owned(),
419 scalar => scalar.to_string(),
420 }
421}
422
423fn integral(number: &Number) -> bool {
425 number.is_i64() || number.is_u64() || number.as_f64().is_some_and(|n| n.fract() == 0.0)
426}
427
428fn same(a: &Value, b: &Value) -> bool {
431 match (a, b) {
432 (Value::Number(a), Value::Number(b)) => {
433 a == b || ((a.is_f64() || b.is_f64()) && a.as_f64() == b.as_f64())
434 }
435 (Value::Array(a), Value::Array(b)) => {
436 a.len() == b.len() && a.iter().zip(b).all(|(a, b)| same(a, b))
437 }
438 (Value::Object(a), Value::Object(b)) => {
439 a.len() == b.len()
440 && a.iter()
441 .all(|(key, a)| b.get(key).is_some_and(|b| same(a, b)))
442 }
443 (a, b) => a == b,
444 }
445}
446
447fn child(at: &str, key: &str) -> String {
449 format!("{at}/{}", key.replace('~', "~0").replace('/', "~1"))
450}
451
452fn problem(at: &str, what: impl Into<String>) -> Problem {
453 Problem {
454 at: at.to_owned(),
455 what: what.into(),
456 }
457}
458
459#[cfg(test)]
460mod tests;