1use serde_json::{Map, Value};
20
21pub fn validate(schema: &Value, value: &Value) -> Result<(), Vec<String>> {
24 let mut errs = Vec::new();
25 let mut depth = 0u32;
26 walk(schema, schema, value, "", &mut errs, &mut depth);
27 if errs.is_empty() { Ok(()) } else { Err(errs) }
28}
29
30pub fn explain(errs: &[String]) -> String {
32 errs.join("; ")
33}
34
35pub fn check_schema(schema: &Value) -> Result<(), Vec<String>> {
39 let mut errs = Vec::new();
40 check_schema_at(schema, "", &mut errs, 0);
41 if errs.is_empty() { Ok(()) } else { Err(errs) }
42}
43
44const MAX_DEPTH: u32 = 64;
45
46fn ptr(path: &str) -> &str {
47 if path.is_empty() { "/" } else { path }
48}
49
50fn walk(
51 root: &Value,
52 schema: &Value,
53 value: &Value,
54 path: &str,
55 errs: &mut Vec<String>,
56 depth: &mut u32,
57) {
58 if *depth > MAX_DEPTH {
59 errs.push(format!("{}: schema nesting exceeds {MAX_DEPTH}", ptr(path)));
60 return;
61 }
62 *depth += 1;
63 walk_inner(root, schema, value, path, errs, depth);
64 *depth -= 1;
65}
66
67fn walk_inner(
68 root: &Value,
69 schema: &Value,
70 value: &Value,
71 path: &str,
72 errs: &mut Vec<String>,
73 depth: &mut u32,
74) {
75 let s = match schema {
76 Value::Bool(true) => return,
77 Value::Bool(false) => {
78 errs.push(format!("{}: no value is allowed here", ptr(path)));
79 return;
80 }
81 Value::Object(s) => s,
82 _ => {
83 errs.push(format!(
84 "{}: invalid schema (not an object or boolean)",
85 ptr(path)
86 ));
87 return;
88 }
89 };
90 if let Some(Value::String(r)) = s.get("$ref") {
92 match resolve_ref(root, r) {
93 Some(target) => walk(root, target, value, path, errs, depth),
94 None => errs.push(format!("{}: unresolvable $ref {r:?}", ptr(path))),
95 }
96 }
97 if value.is_null() && s.get("nullable") == Some(&Value::Bool(true)) {
99 return;
100 }
101 if let Some(t) = s.get("type") {
103 let ok = match t {
104 Value::String(t) => type_matches(t, value),
105 Value::Array(ts) => ts
106 .iter()
107 .any(|t| t.as_str().is_some_and(|t| type_matches(t, value))),
108 _ => true,
109 };
110 if !ok {
111 errs.push(format!(
112 "{}: expected type {}, got {}",
113 ptr(path),
114 render_type(t),
115 type_name(value)
116 ));
117 }
120 }
121 if let Some(Value::Array(e)) = s.get("enum")
123 && !e.iter().any(|x| x == value)
124 {
125 errs.push(format!(
126 "{}: value is not one of the allowed values ({})",
127 ptr(path),
128 short(&Value::Array(e.clone()))
129 ));
130 }
131 if let Some(c) = s.get("const")
132 && c != value
133 {
134 errs.push(format!("{}: value must equal {}", ptr(path), short(c)));
135 }
136 match value {
138 Value::Object(obj) => object_keywords(root, s, obj, path, errs, depth),
139 Value::Array(arr) => array_keywords(root, s, arr, path, errs, depth),
140 Value::String(st) => string_keywords(s, st, path, errs),
141 Value::Number(n) => number_keywords(s, n, path, errs),
142 _ => {}
143 }
144 if let Some(Value::Array(all)) = s.get("allOf") {
146 for sub in all {
147 walk(root, sub, value, path, errs, depth);
148 }
149 }
150 if let Some(Value::Array(any)) = s.get("anyOf") {
151 let mut best: Option<Vec<String>> = None;
152 let mut matched = false;
153 for sub in any {
154 let mut e = Vec::new();
155 walk(root, sub, value, path, &mut e, depth);
156 if e.is_empty() {
157 matched = true;
158 break;
159 }
160 if best.as_ref().is_none_or(|b| e.len() < b.len()) {
161 best = Some(e);
162 }
163 }
164 if !matched {
165 errs.push(format!(
166 "{}: matches none of anyOf ({})",
167 ptr(path),
168 best.map(|b| b.join("; ")).unwrap_or_default()
169 ));
170 }
171 }
172 if let Some(Value::Array(one)) = s.get("oneOf") {
173 let mut n = 0;
174 let mut best: Option<Vec<String>> = None;
175 for sub in one {
176 let mut e = Vec::new();
177 walk(root, sub, value, path, &mut e, depth);
178 if e.is_empty() {
179 n += 1;
180 } else if best.as_ref().is_none_or(|b| e.len() < b.len()) {
181 best = Some(e);
182 }
183 }
184 if n != 1 {
185 errs.push(format!(
186 "{}: must match exactly one of oneOf (matched {n}{})",
187 ptr(path),
188 if n == 0 {
189 format!("; {}", best.map(|b| b.join("; ")).unwrap_or_default())
190 } else {
191 String::new()
192 }
193 ));
194 }
195 }
196 if let Some(not) = s.get("not") {
197 let mut e = Vec::new();
198 walk(root, not, value, path, &mut e, depth);
199 if e.is_empty() {
200 errs.push(format!("{}: must not match the `not` schema", ptr(path)));
201 }
202 }
203 if let Some(cond) = s.get("if") {
204 let mut e = Vec::new();
205 walk(root, cond, value, path, &mut e, depth);
206 let branch = if e.is_empty() {
207 s.get("then")
208 } else {
209 s.get("else")
210 };
211 if let Some(b) = branch {
212 walk(root, b, value, path, errs, depth);
213 }
214 }
215}
216
217fn object_keywords(
218 root: &Value,
219 s: &Map<String, Value>,
220 obj: &Map<String, Value>,
221 path: &str,
222 errs: &mut Vec<String>,
223 depth: &mut u32,
224) {
225 if let Some(Value::Array(req)) = s.get("required") {
226 for r in req {
227 if let Some(r) = r.as_str()
228 && !obj.contains_key(r)
229 {
230 errs.push(format!("{}: missing required property {r:?}", ptr(path)));
231 }
232 }
233 }
234 let props = s.get("properties").and_then(Value::as_object);
235 let pattern_props = s.get("patternProperties").and_then(Value::as_object);
236 let additional = s.get("additionalProperties");
237 for (k, v) in obj {
238 let child = format!("{path}/{}", escape(k));
239 let mut covered = false;
240 if let Some(sub) = props.and_then(|p| p.get(k)) {
241 covered = true;
242 walk(root, sub, v, &child, errs, depth);
243 }
244 if let Some(pp) = pattern_props {
245 for (pat, sub) in pp {
246 if literal_pattern_matches(pat, k) {
247 covered = true;
248 walk(root, sub, v, &child, errs, depth);
249 }
250 }
251 }
252 if !covered {
253 match additional {
254 Some(Value::Bool(false)) => {
255 errs.push(format!("{}: unknown property {k:?}", ptr(path)))
256 }
257 Some(sub @ Value::Object(_)) => walk(root, sub, v, &child, errs, depth),
258 _ => {}
259 }
260 }
261 }
262 if let Some(n) = s.get("minProperties").and_then(Value::as_u64)
263 && (obj.len() as u64) < n
264 {
265 errs.push(format!("{}: at least {n} properties required", ptr(path)));
266 }
267 if let Some(n) = s.get("maxProperties").and_then(Value::as_u64)
268 && (obj.len() as u64) > n
269 {
270 errs.push(format!("{}: at most {n} properties allowed", ptr(path)));
271 }
272 if let Some(Value::Object(deps)) = s.get("dependentRequired") {
273 for (k, needs) in deps {
274 if obj.contains_key(k)
275 && let Some(needs) = needs.as_array()
276 {
277 for n in needs.iter().filter_map(Value::as_str) {
278 if !obj.contains_key(n) {
279 errs.push(format!("{}: property {k:?} requires {n:?}", ptr(path)));
280 }
281 }
282 }
283 }
284 }
285}
286
287fn array_keywords(
288 root: &Value,
289 s: &Map<String, Value>,
290 arr: &[Value],
291 path: &str,
292 errs: &mut Vec<String>,
293 depth: &mut u32,
294) {
295 let prefix = s.get("prefixItems").and_then(Value::as_array);
296 let items = s.get("items");
297 for (i, v) in arr.iter().enumerate() {
298 let child = format!("{path}/{i}");
299 if let Some(p) = prefix.and_then(|p| p.get(i)) {
300 walk(root, p, v, &child, errs, depth);
301 continue;
302 }
303 match items {
304 Some(Value::Bool(false)) if prefix.is_some() => {
305 errs.push(format!("{}: no additional items allowed", ptr(&child)));
306 }
307 Some(sub @ (Value::Object(_) | Value::Bool(_))) => {
308 walk(root, sub, v, &child, errs, depth)
309 }
310 _ => {}
311 }
312 }
313 if let Some(n) = s.get("minItems").and_then(Value::as_u64)
314 && (arr.len() as u64) < n
315 {
316 errs.push(format!("{}: at least {n} items required", ptr(path)));
317 }
318 if let Some(n) = s.get("maxItems").and_then(Value::as_u64)
319 && (arr.len() as u64) > n
320 {
321 errs.push(format!("{}: at most {n} items allowed", ptr(path)));
322 }
323 if s.get("uniqueItems") == Some(&Value::Bool(true)) {
324 for i in 0..arr.len() {
325 if arr[i + 1..].contains(&arr[i]) {
326 errs.push(format!(
327 "{}: items must be unique (duplicate at {i})",
328 ptr(path)
329 ));
330 break;
331 }
332 }
333 }
334 if let Some(c) = s.get("contains") {
335 let any = arr.iter().any(|v| {
336 let mut e = Vec::new();
337 walk(root, c, v, path, &mut e, depth);
338 e.is_empty()
339 });
340 if !any {
341 errs.push(format!("{}: no item matches `contains`", ptr(path)));
342 }
343 }
344}
345
346fn string_keywords(s: &Map<String, Value>, st: &str, path: &str, errs: &mut Vec<String>) {
347 let len = st.chars().count() as u64;
348 if let Some(n) = s.get("minLength").and_then(Value::as_u64)
349 && len < n
350 {
351 errs.push(format!("{}: at least {n} characters required", ptr(path)));
352 }
353 if let Some(n) = s.get("maxLength").and_then(Value::as_u64)
354 && len > n
355 {
356 errs.push(format!("{}: at most {n} characters allowed", ptr(path)));
357 }
358 if let Some(Value::String(p)) = s.get("pattern")
359 && is_literal_pattern(p)
360 && !literal_pattern_matches(p, st)
361 {
362 errs.push(format!("{}: does not match pattern {p:?}", ptr(path)));
363 }
364}
365
366fn number_keywords(
367 s: &Map<String, Value>,
368 n: &serde_json::Number,
369 path: &str,
370 errs: &mut Vec<String>,
371) {
372 let Some(x) = n.as_f64() else { return };
373 let num = |k: &str| s.get(k).and_then(Value::as_f64);
374 if let Some(m) = num("minimum")
375 && x < m
376 {
377 errs.push(format!("{}: must be >= {m}", ptr(path)));
378 }
379 if let Some(m) = num("maximum")
380 && x > m
381 {
382 errs.push(format!("{}: must be <= {m}", ptr(path)));
383 }
384 if let Some(m) = num("exclusiveMinimum")
385 && x <= m
386 {
387 errs.push(format!("{}: must be > {m}", ptr(path)));
388 }
389 if let Some(m) = num("exclusiveMaximum")
390 && x >= m
391 {
392 errs.push(format!("{}: must be < {m}", ptr(path)));
393 }
394 if let Some(m) = num("multipleOf")
395 && m > 0.0
396 {
397 let q = x / m;
398 if (q - q.round()).abs() > 1e-9 {
399 errs.push(format!("{}: must be a multiple of {m}", ptr(path)));
400 }
401 }
402}
403
404fn type_matches(t: &str, v: &Value) -> bool {
405 match t {
406 "null" => v.is_null(),
407 "boolean" => v.is_boolean(),
408 "object" => v.is_object(),
409 "array" => v.is_array(),
410 "string" => v.is_string(),
411 "number" => v.is_number(),
412 "integer" => {
413 v.as_i64().is_some()
414 || v.as_u64().is_some()
415 || v.as_f64().is_some_and(|f| f.fract() == 0.0)
416 }
417 _ => true, }
419}
420
421fn type_name(v: &Value) -> &'static str {
422 match v {
423 Value::Null => "null",
424 Value::Bool(_) => "boolean",
425 Value::Number(_) => "number",
426 Value::String(_) => "string",
427 Value::Array(_) => "array",
428 Value::Object(_) => "object",
429 }
430}
431
432fn render_type(t: &Value) -> String {
433 match t {
434 Value::String(s) => s.clone(),
435 Value::Array(a) => a
436 .iter()
437 .filter_map(Value::as_str)
438 .collect::<Vec<_>>()
439 .join("|"),
440 _ => "?".into(),
441 }
442}
443
444fn short(v: &Value) -> String {
445 let s = v.to_string();
446 if s.len() > 80 {
447 let mut cut = 77;
448 while !s.is_char_boundary(cut) {
449 cut -= 1;
450 }
451 format!("{}…", &s[..cut])
452 } else {
453 s
454 }
455}
456
457fn escape(k: &str) -> String {
458 k.replace('~', "~0").replace('/', "~1")
459}
460
461fn resolve_ref<'a>(root: &'a Value, r: &str) -> Option<&'a Value> {
463 let rest = r.strip_prefix('#')?;
464 if rest.is_empty() {
465 return Some(root);
466 }
467 let mut cur = root;
468 for seg in rest.trim_start_matches('/').split('/') {
469 let seg = seg.replace("~1", "/").replace("~0", "~");
470 cur = match cur {
471 Value::Object(m) => m.get(&seg)?,
472 Value::Array(a) => a.get(seg.parse::<usize>().ok()?)?,
473 _ => return None,
474 };
475 }
476 Some(cur)
477}
478
479fn is_literal_pattern(p: &str) -> bool {
485 let core = p.trim_start_matches('^').trim_end_matches('$');
486 core == ".*" || !core.chars().any(|c| ".*+?()[]{}|\\".contains(c))
487}
488
489fn literal_pattern_matches(p: &str, s: &str) -> bool {
490 if !is_literal_pattern(p) {
491 return true; }
493 let anchored_start = p.starts_with('^');
494 let anchored_end = p.ends_with('$');
495 let core = p.trim_start_matches('^').trim_end_matches('$');
496 if core == ".*" {
497 return true;
498 }
499 match (anchored_start, anchored_end) {
500 (true, true) => s == core,
501 (true, false) => s.starts_with(core),
502 (false, true) => s.ends_with(core),
503 (false, false) => s.contains(core),
504 }
505}
506
507fn check_schema_at(schema: &Value, path: &str, errs: &mut Vec<String>, depth: u32) {
508 if depth > MAX_DEPTH {
509 errs.push(format!("{}: schema nesting exceeds {MAX_DEPTH}", ptr(path)));
510 return;
511 }
512 let s = match schema {
513 Value::Bool(_) => return,
514 Value::Object(s) => s,
515 _ => {
516 errs.push(format!(
517 "{}: a schema must be an object or a boolean",
518 ptr(path)
519 ));
520 return;
521 }
522 };
523 let sub = |k: &str, v: &Value, errs: &mut Vec<String>| {
524 check_schema_at(v, &format!("{path}/{k}"), errs, depth + 1)
525 };
526 for (k, v) in s {
527 match k.as_str() {
528 "type" => {
529 let ok = match v {
530 Value::String(t) => KNOWN_TYPES.contains(&t.as_str()),
531 Value::Array(a) => a
532 .iter()
533 .all(|t| t.as_str().is_some_and(|t| KNOWN_TYPES.contains(&t))),
534 _ => false,
535 };
536 if !ok {
537 errs.push(format!(
538 "{}/type: must be a known type name or a list of them",
539 ptr(path)
540 ));
541 }
542 }
543 "properties" | "patternProperties" | "$defs" | "definitions" => match v {
544 Value::Object(m) => {
545 for (name, sv) in m {
546 check_schema_at(
547 sv,
548 &format!("{path}/{k}/{}", escape(name)),
549 errs,
550 depth + 1,
551 );
552 }
553 }
554 _ => errs.push(format!("{}/{k}: must be an object of schemas", ptr(path))),
555 },
556 "items" | "additionalProperties" | "not" | "if" | "then" | "else" | "contains" => {
557 sub(k, v, errs)
558 }
559 "prefixItems" | "allOf" | "anyOf" | "oneOf" => match v {
560 Value::Array(a) => {
561 for (i, sv) in a.iter().enumerate() {
562 check_schema_at(sv, &format!("{path}/{k}/{i}"), errs, depth + 1);
563 }
564 }
565 _ => errs.push(format!("{}/{k}: must be an array of schemas", ptr(path))),
566 },
567 "required" => {
568 if !v.as_array().is_some_and(|a| a.iter().all(Value::is_string)) {
569 errs.push(format!(
570 "{}/required: must be an array of property names",
571 ptr(path)
572 ));
573 }
574 }
575 "enum" => {
576 if !v.is_array() {
577 errs.push(format!("{}/enum: must be an array", ptr(path)));
578 }
579 }
580 "minimum" | "maximum" | "exclusiveMinimum" | "exclusiveMaximum" | "multipleOf" => {
581 if !v.is_number() {
582 errs.push(format!("{}/{k}: must be a number", ptr(path)));
583 }
584 }
585 "minLength" | "maxLength" | "minItems" | "maxItems" | "minProperties"
586 | "maxProperties" => {
587 if v.as_u64().is_none() {
588 errs.push(format!("{}/{k}: must be a non-negative integer", ptr(path)));
589 }
590 }
591 "pattern" => {
592 if let Some(p) = v.as_str() {
593 if !is_literal_pattern(p) {
594 errs.push(format!(
595 "{}/pattern: {p:?} uses regex features this validator does not enforce (literal, ^prefix, suffix$, ^exact$ only)",
596 ptr(path)
597 ));
598 }
599 } else {
600 errs.push(format!("{}/pattern: must be a string", ptr(path)));
601 }
602 }
603 "$ref" if !v.as_str().is_some_and(|r| r.starts_with('#')) => {
604 errs.push(format!(
605 "{}/$ref: only local references (`#/...`) are supported",
606 ptr(path)
607 ));
608 }
609 _ => {}
610 }
611 }
612}
613
614const KNOWN_TYPES: &[&str] = &[
615 "null", "boolean", "object", "array", "string", "number", "integer",
616];
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use serde_json::json;
622
623 fn ok(s: &Value, v: &Value) {
624 if let Err(e) = validate(s, v) {
625 panic!("expected valid, got {e:?}");
626 }
627 }
628 fn bad(s: &Value, v: &Value) -> Vec<String> {
629 validate(s, v).expect_err("expected invalid")
630 }
631
632 #[test]
633 fn types_required_and_additional_properties() {
634 let s = json!({
635 "type": "object",
636 "properties": {
637 "name": {"type": "string", "minLength": 1},
638 "n": {"type": "integer", "minimum": 0},
639 "tags": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
640 "mode": {"enum": ["a", "b"]},
641 "x": {"type": ["number", "null"]}
642 },
643 "required": ["name"],
644 "additionalProperties": false
645 });
646 ok(
647 &s,
648 &json!({"name": "k", "n": 3, "tags": ["a", "b"], "mode": "a", "x": null}),
649 );
650 ok(&s, &json!({"name": "k", "n": 3.0}));
651 let e = bad(
652 &s,
653 &json!({"n": -1.5, "tags": ["a", "a"], "mode": "z", "extra": 1, "x": "s"}),
654 );
655 let joined = e.join("\n");
656 assert!(
657 joined.contains("/: missing required property \"name\""),
658 "{joined}"
659 );
660 assert!(joined.contains("/n: expected type integer"), "{joined}");
661 assert!(joined.contains("/n: must be >= 0"), "{joined}");
662 assert!(joined.contains("/tags: items must be unique"), "{joined}");
663 assert!(
664 joined.contains("/mode: value is not one of the allowed values"),
665 "{joined}"
666 );
667 assert!(joined.contains("/: unknown property \"extra\""), "{joined}");
668 assert!(
669 joined.contains("/x: expected type number|null, got string"),
670 "{joined}"
671 );
672 ok(&json!(true), &json!(42));
674 assert!(!bad(&json!(false), &json!(42)).is_empty());
675 ok(
677 &json!({"properties": {"a": {"type": "string"}}}),
678 &json!("str"),
679 );
680 }
681
682 #[test]
683 fn combinators_refs_and_conditionals() {
684 let s = json!({
685 "$defs": {"pos": {"type": "integer", "exclusiveMinimum": 0}},
686 "type": "object",
687 "properties": {
688 "id": {"$ref": "#/$defs/pos"},
689 "kind": {"type": "string"},
690 "v": {"oneOf": [{"type": "string"}, {"type": "integer"}]},
691 "w": {"anyOf": [{"type": "string", "maxLength": 2}, {"type": "boolean"}]},
692 "z": {"not": {"const": 0}},
693 "self": {"$ref": "#"}
694 },
695 "if": {"properties": {"kind": {"const": "a"}}, "required": ["kind"]},
696 "then": {"required": ["v"]},
697 "else": {"required": ["w"]}
698 });
699 ok(&s, &json!({"id": 1, "kind": "a", "v": "x", "z": 1}));
700 ok(
701 &s,
702 &json!({"id": 2, "kind": "b", "w": true, "self": {"id": 3, "w": "ab"}}),
703 );
704 let e = bad(
705 &s,
706 &json!({"id": 0, "kind": "a", "w": "abc", "z": 0, "self": {"id": -1, "kind": "a"}}),
707 );
708 let joined = e.join("\n");
709 assert!(joined.contains("/id: must be > 0"), "{joined}");
710 assert!(
711 joined.contains("/: missing required property \"v\""),
712 "{joined}"
713 );
714 assert!(joined.contains("/z: must not match"), "{joined}");
715 assert!(joined.contains("/self/id: must be > 0"), "{joined}");
716 assert!(
717 joined.contains("/self: missing required property \"v\""),
718 "{joined}"
719 );
720 let e = bad(&s, &json!({"id": 1, "kind": "b", "w": "abc"}));
721 assert!(e.join("\n").contains("/w: matches none of anyOf"), "{e:?}");
722 let e = bad(&s, &json!({"id": 1, "kind": "b", "w": "a", "v": 1.5}));
723 assert!(
724 e.join("\n")
725 .contains("/v: must match exactly one of oneOf (matched 0"),
726 "{e:?}"
727 );
728 assert!(bad(&json!({"$ref": "#/$defs/nope"}), &json!(1))[0].contains("unresolvable $ref"));
729 }
730
731 #[test]
732 fn arrays_strings_numbers_and_patterns() {
733 let s = json!({
734 "type": "array",
735 "prefixItems": [{"type": "string"}, {"type": "number"}],
736 "items": {"type": "boolean"},
737 "minItems": 2, "maxItems": 4,
738 "contains": {"const": true}
739 });
740 ok(&s, &json!(["a", 1, true]));
741 let e = bad(&s, &json!(["a", "b", 1, false, true]));
742 let joined = e.join("\n");
743 assert!(joined.contains("/1: expected type number"), "{joined}");
744 assert!(joined.contains("/2: expected type boolean"), "{joined}");
745 assert!(joined.contains("/: at most 4 items"), "{joined}");
746 assert!(
747 bad(&s, &json!(["a", 1, false]))
748 .join("")
749 .contains("contains")
750 );
751 let s = json!({"type": "string", "pattern": "^agentd/", "maxLength": 12});
752 ok(&s, &json!("agentd/x"));
753 assert!(bad(&s, &json!("other/x"))[0].contains("pattern"));
754 assert!(bad(&s, &json!("agentd/toolongvalue"))[0].contains("at most 12"));
755 ok(&json!({"pattern": "^[a-z]+$"}), &json!("123"));
757 assert!(
758 check_schema(&json!({"pattern": "^[a-z]+$"})).unwrap_err()[0]
759 .contains("regex features")
760 );
761 let s = json!({"type": "number", "multipleOf": 0.5, "maximum": 10, "exclusiveMaximum": 10});
762 ok(&s, &json!(9.5));
763 let e = bad(&s, &json!(10));
764 assert!(e.iter().any(|m| m.contains("must be < 10")), "{e:?}");
765 assert!(bad(&s, &json!(9.3))[0].contains("multiple of 0.5"));
766 let s = json!({"type": "object", "patternProperties": {"^x-": {"type": "string"}}, "additionalProperties": false});
768 ok(&s, &json!({"x-team": "ops"}));
769 assert!(bad(&s, &json!({"x-team": 1}))[0].contains("/x-team: expected type string"));
770 assert!(bad(&s, &json!({"team": "ops"}))[0].contains("unknown property"));
771 let s = json!({"type": "object", "dependentRequired": {"a": ["b"]}});
773 ok(&s, &json!({"a": 1, "b": 2}));
774 assert!(bad(&s, &json!({"a": 1}))[0].contains("requires \"b\""));
775 ok(&json!({"type": "integer"}), &json!(3.0));
777 assert!(!bad(&json!({"type": "integer"}), &json!(3.5)).is_empty());
778 }
779
780 #[test]
781 fn schema_well_formedness() {
782 assert!(check_schema(&json!({"type": "object", "properties": {"a": {"type": "string"}}, "required": ["a"]})).is_ok());
783 let e = check_schema(&json!({"type": "strng", "properties": [], "required": "a", "items": 3, "minLength": -1, "$ref": "http://x"}))
784 .unwrap_err();
785 let joined = e.join("\n");
786 assert!(joined.contains("/type: must be a known type"), "{joined}");
787 assert!(
788 joined.contains("/properties: must be an object"),
789 "{joined}"
790 );
791 assert!(joined.contains("/required: must be an array"), "{joined}");
792 assert!(
793 joined.contains("/items: a schema must be an object or a boolean"),
794 "{joined}"
795 );
796 assert!(
797 joined.contains("/minLength: must be a non-negative integer"),
798 "{joined}"
799 );
800 assert!(joined.contains("/$ref: only local references"), "{joined}");
801 assert!(check_schema(&json!(true)).is_ok());
802 }
803}