1use fastmcp_core::AbsoluteUri;
19use regex::Regex;
20use serde_json::Value;
21use std::{cmp::Ordering, collections::HashSet, fmt};
22
23pub const FINAL_JSON_SCHEMA_DIALECT: &str = "https://json-schema.org/draft/2020-12/schema";
26
27const CORE_VOCABULARY_URI: &str = "https://json-schema.org/draft/2020-12/vocab/core";
28const APPLICATOR_VOCABULARY_URI: &str = "https://json-schema.org/draft/2020-12/vocab/applicator";
29const UNEVALUATED_VOCABULARY_URI: &str = "https://json-schema.org/draft/2020-12/vocab/unevaluated";
30const VALIDATION_VOCABULARY_URI: &str = "https://json-schema.org/draft/2020-12/vocab/validation";
31const META_DATA_VOCABULARY_URI: &str = "https://json-schema.org/draft/2020-12/vocab/meta-data";
32const FORMAT_ANNOTATION_VOCABULARY_URI: &str =
33 "https://json-schema.org/draft/2020-12/vocab/format-annotation";
34const FORMAT_ASSERTION_VOCABULARY_URI: &str =
35 "https://json-schema.org/draft/2020-12/vocab/format-assertion";
36const CONTENT_VOCABULARY_URI: &str = "https://json-schema.org/draft/2020-12/vocab/content";
37
38pub const MAX_SCHEMA_VALIDATION_DEPTH: usize = 64;
40
41pub const MAX_SCHEMA_ADMISSION_NODES: usize = 4_096;
43
44pub const MAX_SCHEMA_INSTANCE_NODES: usize = 4_096;
46
47pub const MAX_SCHEMA_INSTANCE_DEPTH: usize = 64;
49
50pub const MAX_SCHEMA_INSTANCE_STRING_BYTES: usize = 64 * 1024;
52
53pub const MAX_SCHEMA_VALIDATION_WORK: usize = 4_096;
57
58pub const MAX_LOCAL_REFERENCE_DEPTH: usize = 32;
60
61pub const MAX_COMPOSITION_BRANCHES: usize = 64;
63
64pub const MAX_PATTERN_PROPERTIES: usize = 64;
66
67pub const MAX_PATTERN_PROPERTY_BYTES: usize = 4 * 1024;
69
70pub const MAX_PATTERN_BYTES: usize = 4 * 1024;
72
73pub const MAX_SCHEMA_ASSERTION_ENTRIES: usize = 64;
75
76pub const MAX_SCHEMA_ASSERTION_STRING_BYTES: usize = 4 * 1024;
78
79pub const MAX_VALIDATION_ERRORS: usize = 64;
81
82const MAX_EXACT_DECIMAL_DIGITS: usize = 4 * 1024;
88
89#[derive(Debug, Clone)]
91pub struct ValidationError {
92 pub path: String,
94 pub message: String,
96}
97
98impl fmt::Display for ValidationError {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 write!(f, "{}: {}", self.path, self.message)
101 }
102}
103
104impl std::error::Error for ValidationError {}
105
106pub type ValidationResult = Result<(), Vec<ValidationError>>;
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct SchemaAdmissionError {
112 path: String,
113 reason: &'static str,
114}
115
116impl SchemaAdmissionError {
117 fn new(path: impl Into<String>, reason: &'static str) -> Self {
118 Self {
119 path: path.into(),
120 reason,
121 }
122 }
123
124 #[must_use]
126 pub fn path(&self) -> &str {
127 &self.path
128 }
129
130 #[must_use]
132 pub const fn reason(&self) -> &'static str {
133 self.reason
134 }
135}
136
137impl fmt::Display for SchemaAdmissionError {
138 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
139 write!(formatter, "{}: {}", self.path, self.reason)
140 }
141}
142
143impl std::error::Error for SchemaAdmissionError {}
144
145#[derive(Debug, Clone)]
150pub struct AdmittedSchema {
151 schema: Value,
152}
153
154impl AdmittedSchema {
155 #[must_use]
157 pub const fn schema(&self) -> &Value {
158 &self.schema
159 }
160
161 pub fn validate(&self, value: &Value) -> ValidationResult {
163 validate_admitted_final_schema(&self.schema, value)
164 }
165}
166
167#[derive(Debug, Clone)]
174pub struct AdmittedFinalFormSchema {
175 schema: AdmittedSchema,
176}
177
178impl AdmittedFinalFormSchema {
179 #[must_use]
181 pub const fn schema(&self) -> &Value {
182 self.schema.schema()
183 }
184}
185
186impl serde::Serialize for AdmittedFinalFormSchema {
187 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
188 where
189 S: serde::Serializer,
190 {
191 serde::Serialize::serialize(self.schema(), serializer)
192 }
193}
194
195impl<'de> serde::Deserialize<'de> for AdmittedFinalFormSchema {
196 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
197 where
198 D: serde::Deserializer<'de>,
199 {
200 Self::admit(<Value as serde::Deserialize>::deserialize(deserializer)?)
201 .map_err(serde::de::Error::custom)
202 }
203}
204
205impl AdmittedFinalFormSchema {
206 pub fn admit(schema: Value) -> Result<Self, SchemaAdmissionError> {
208 admit_final_form_schema(schema)
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub enum FinalCoreResultType {
215 Complete,
217 InputRequired,
219}
220
221impl FinalCoreResultType {
222 #[must_use]
224 pub const fn as_str(self) -> &'static str {
225 match self {
226 Self::Complete => "complete",
227 Self::InputRequired => "input_required",
228 }
229 }
230}
231
232pub fn admit_final_schema(schema: Value) -> Result<AdmittedSchema, SchemaAdmissionError> {
241 let mut node_count = 0;
242 validate_final_schema_node(&schema, &schema, "$", true, 0, &mut node_count)?;
243 validate_unique_local_anchors(&schema, "$", 0, &mut HashSet::new())?;
244 validate_unique_local_resource_ids(&schema, "$", 0, None, &mut HashSet::new())?;
245 Ok(AdmittedSchema { schema })
246}
247
248pub fn admit_final_form_schema(
256 schema: Value,
257) -> Result<AdmittedFinalFormSchema, SchemaAdmissionError> {
258 let schema = admit_final_schema(schema)?;
259 validate_final_form_schema(schema.schema())?;
260 Ok(AdmittedFinalFormSchema { schema })
261}
262
263fn validate_final_form_schema(schema: &Value) -> Result<(), SchemaAdmissionError> {
264 let root = schema
265 .as_object()
266 .ok_or_else(|| SchemaAdmissionError::new("$", "final form schema must be an object"))?;
267 if root.get("type").and_then(Value::as_str) != Some("object") {
268 return Err(SchemaAdmissionError::new(
269 "$.type",
270 "final form schema type must be object",
271 ));
272 }
273 for keyword in [
274 "$ref",
275 "$dynamicRef",
276 "$defs",
277 "items",
278 "prefixItems",
279 "contains",
280 "allOf",
281 "anyOf",
282 "oneOf",
283 "not",
284 "if",
285 "then",
286 "else",
287 "dependentSchemas",
288 "patternProperties",
289 "propertyNames",
290 "unevaluatedProperties",
291 "unevaluatedItems",
292 "contentSchema",
293 ] {
294 if root.contains_key(keyword) {
295 return Err(SchemaAdmissionError::new(
296 format!("$.{keyword}"),
297 "final form schema cannot contain nested or composed schemas",
298 ));
299 }
300 }
301 let properties = root.get("properties").map_or(Ok(None), |value| {
302 value.as_object().map(Some).ok_or_else(|| {
303 SchemaAdmissionError::new("$.properties", "final form properties must be an object")
304 })
305 })?;
306 if root
307 .get("additionalProperties")
308 .is_some_and(|value| value != &Value::Bool(false))
309 {
310 return Err(SchemaAdmissionError::new(
311 "$.additionalProperties",
312 "final form additionalProperties must be false when present",
313 ));
314 }
315 if let Some(required) = root.get("required") {
316 let Some(required) = required.as_array() else {
317 return Err(SchemaAdmissionError::new(
318 "$.required",
319 "final form required must be an array",
320 ));
321 };
322 for (index, name) in required.iter().enumerate() {
323 let Some(name) = name.as_str() else {
324 return Err(SchemaAdmissionError::new(
325 format!("$.required[{index}]"),
326 "final form required entries must be property names",
327 ));
328 };
329 if !properties.is_some_and(|properties| properties.contains_key(name)) {
330 return Err(SchemaAdmissionError::new(
331 format!("$.required[{index}]"),
332 "final form required entries must name declared properties",
333 ));
334 }
335 }
336 }
337 let Some(properties) = properties else {
338 return Ok(());
339 };
340 for (name, property) in properties {
341 validate_final_form_property_schema(name, property)?;
342 }
343 Ok(())
344}
345
346fn validate_final_form_property_schema(
347 name: &str,
348 property: &Value,
349) -> Result<(), SchemaAdmissionError> {
350 let path = format!("$.properties.{name}");
351 let property = property.as_object().ok_or_else(|| {
352 SchemaAdmissionError::new(&path, "final form property schema must be an object")
353 })?;
354 match property.get("type").and_then(Value::as_str) {
355 Some("string" | "number" | "integer" | "boolean") => {}
356 _ => {
357 return Err(SchemaAdmissionError::new(
358 format!("{path}.type"),
359 "final form property type must be a primitive",
360 ));
361 }
362 }
363 for keyword in [
364 "$ref",
365 "$dynamicRef",
366 "$defs",
367 "properties",
368 "items",
369 "prefixItems",
370 "contains",
371 "allOf",
372 "anyOf",
373 "oneOf",
374 "not",
375 "if",
376 "then",
377 "else",
378 "dependentSchemas",
379 "patternProperties",
380 "propertyNames",
381 "unevaluatedProperties",
382 "unevaluatedItems",
383 "contentSchema",
384 ] {
385 if property.contains_key(keyword) {
386 return Err(SchemaAdmissionError::new(
387 format!("{path}.{keyword}"),
388 "final form property cannot contain nested or composed schemas",
389 ));
390 }
391 }
392 Ok(())
393}
394
395pub fn validate_final_core_result(
402 schema: &AdmittedSchema,
403 value: &Value,
404 expected_result_type: FinalCoreResultType,
405) -> ValidationResult {
406 let mut errors = Vec::new();
407 let Some(result) = value.as_object() else {
408 push_error(&mut errors, "root", "final result must be an object");
409 return Err(errors);
410 };
411
412 match result.get("resultType") {
413 Some(Value::String(result_type)) if result_type == expected_result_type.as_str() => {}
414 Some(Value::String(_)) => push_error(
415 &mut errors,
416 "root.resultType",
417 "resultType does not match the selected final core result branch",
418 ),
419 Some(_) => push_error(
420 &mut errors,
421 "root.resultType",
422 "resultType must be a final core result discriminator string",
423 ),
424 None => push_error(&mut errors, "root", "final result requires resultType"),
425 }
426
427 if let Err(schema_errors) = schema.validate(value) {
428 for error in schema_errors {
429 if errors.len() == MAX_VALIDATION_ERRORS {
430 break;
431 }
432 errors.push(error);
433 }
434 }
435
436 if errors.is_empty() {
437 Ok(())
438 } else {
439 Err(errors)
440 }
441}
442
443fn validate_final_schema_node(
444 schema: &Value,
445 root_schema: &Value,
446 path: &str,
447 root: bool,
448 depth: usize,
449 node_count: &mut usize,
450) -> Result<(), SchemaAdmissionError> {
451 if depth >= MAX_SCHEMA_VALIDATION_DEPTH {
452 return Err(SchemaAdmissionError::new(
453 path,
454 "schema admission nesting limit exceeded",
455 ));
456 }
457 *node_count += 1;
458 if *node_count > MAX_SCHEMA_ADMISSION_NODES {
459 return Err(SchemaAdmissionError::new(
460 path,
461 "schema admission node limit exceeded",
462 ));
463 }
464 if schema.is_boolean() {
465 return Ok(());
466 }
467 let object = schema
468 .as_object()
469 .ok_or_else(|| SchemaAdmissionError::new(path, "schema must be an object or boolean"))?;
470
471 validate_supported_schema_keywords(object, path, root)?;
472 validate_schema_id_keyword(object, path)?;
473 validate_schema_dialect_keyword(object, root_schema, path)?;
474
475 validate_local_reference_keyword(object, "$ref", root_schema, path)?;
476 validate_local_reference_keyword(object, "$dynamicRef", root_schema, path)?;
477 validate_anchor_keyword(object, "$anchor", path)?;
478 validate_anchor_keyword(object, "$dynamicAnchor", path)?;
479
480 if let Some(type_value) = object.get("type") {
481 validate_schema_type(type_value, &format!("{path}.type"))?;
482 }
483 validate_string_array_keyword(object, "required", path)?;
484 validate_string_array_keyword(object, "dependentRequired", path)?;
485 validate_nonnegative_integer_keywords(
486 object,
487 path,
488 &[
489 "minProperties",
490 "maxProperties",
491 "minItems",
492 "maxItems",
493 "minContains",
494 "maxContains",
495 "minLength",
496 "maxLength",
497 ],
498 )?;
499 validate_number_keywords(
500 object,
501 path,
502 &[
503 "minimum",
504 "maximum",
505 "exclusiveMinimum",
506 "exclusiveMaximum",
507 "multipleOf",
508 ],
509 )?;
510 if object.get("multipleOf").is_some_and(|value| {
511 ExactDecimal::from_value(value).is_none_or(|multiple| !multiple.is_positive())
512 }) {
513 return Err(SchemaAdmissionError::new(
514 format!("{path}.multipleOf"),
515 "multipleOf must be a positive number",
516 ));
517 }
518 validate_boolean_keywords(object, path, &["uniqueItems"])?;
519 validate_enum_keyword(object, path)?;
520 if let Some(value) = object.get("const") {
521 validate_exact_equality_value(value, &format!("{path}.const"), depth + 1, node_count)?;
522 }
523 if let Some(values) = object.get("enum").and_then(Value::as_array) {
524 for (index, value) in values.iter().enumerate() {
525 validate_exact_equality_value(
526 value,
527 &format!("{path}.enum[{index}]"),
528 depth + 1,
529 node_count,
530 )?;
531 }
532 }
533 validate_pattern_keyword(object, path)?;
534 validate_format_keyword(object, path)?;
535 validate_content_annotation_keywords(object, path)?;
536
537 for keyword in [
538 "properties",
539 "patternProperties",
540 "$defs",
541 "dependentSchemas",
542 ] {
543 if let Some(subschemas) = object.get(keyword) {
544 let subschemas = subschemas.as_object().ok_or_else(|| {
545 SchemaAdmissionError::new(
546 format!("{path}.{keyword}"),
547 "schema map keyword must be an object",
548 )
549 })?;
550 if keyword == "patternProperties" {
551 if subschemas.len() > MAX_PATTERN_PROPERTIES {
552 return Err(SchemaAdmissionError::new(
553 format!("{path}.{keyword}"),
554 "patternProperties exceeds entry limit",
555 ));
556 }
557 for pattern in subschemas.keys() {
558 if pattern.len() > MAX_PATTERN_PROPERTY_BYTES {
559 return Err(SchemaAdmissionError::new(
560 format!("{path}.{keyword}"),
561 "patternProperties pattern exceeds byte limit",
562 ));
563 }
564 if Regex::new(pattern).is_err() {
565 return Err(SchemaAdmissionError::new(
566 format!("{path}.{keyword}"),
567 "invalid patternProperties pattern",
568 ));
569 }
570 }
571 }
572 for (name, subschema) in subschemas {
573 validate_final_schema_node(
574 subschema,
575 root_schema,
576 &format!("{path}.{keyword}.{name}"),
577 false,
578 depth + 1,
579 node_count,
580 )?;
581 }
582 }
583 }
584
585 for keyword in [
586 "additionalProperties",
587 "unevaluatedProperties",
588 "unevaluatedItems",
589 "items",
590 "contains",
591 "not",
592 "if",
593 "then",
594 "else",
595 "propertyNames",
596 "contentSchema",
597 ] {
598 if let Some(subschema) = object.get(keyword) {
599 validate_final_schema_node(
600 subschema,
601 root_schema,
602 &format!("{path}.{keyword}"),
603 false,
604 depth + 1,
605 node_count,
606 )?;
607 }
608 }
609
610 for keyword in ["prefixItems", "allOf", "anyOf", "oneOf"] {
611 if let Some(subschemas) = object.get(keyword) {
612 let subschemas = subschemas.as_array().ok_or_else(|| {
613 SchemaAdmissionError::new(
614 format!("{path}.{keyword}"),
615 "schema array keyword must be an array",
616 )
617 })?;
618 if matches!(keyword, "allOf" | "anyOf" | "oneOf")
619 && subschemas.len() > MAX_COMPOSITION_BRANCHES
620 {
621 return Err(SchemaAdmissionError::new(
622 format!("{path}.{keyword}"),
623 "composition keyword exceeds branch limit",
624 ));
625 }
626 for (index, subschema) in subschemas.iter().enumerate() {
627 validate_final_schema_node(
628 subschema,
629 root_schema,
630 &format!("{path}.{keyword}[{index}]"),
631 false,
632 depth + 1,
633 node_count,
634 )?;
635 }
636 }
637 }
638
639 Ok(())
640}
641
642fn validate_supported_schema_keywords(
643 object: &serde_json::Map<String, Value>,
644 path: &str,
645 root: bool,
646) -> Result<(), SchemaAdmissionError> {
647 const SUPPORTED: &[&str] = &[
648 "$anchor",
649 "$comment",
650 "$defs",
651 "$dynamicAnchor",
652 "$dynamicRef",
653 "$id",
654 "$ref",
655 "$schema",
656 "$vocabulary",
657 "additionalProperties",
658 "allOf",
659 "anyOf",
660 "const",
661 "contains",
662 "contentEncoding",
663 "contentMediaType",
664 "contentSchema",
665 "default",
666 "dependentRequired",
667 "dependentSchemas",
668 "deprecated",
669 "description",
670 "else",
671 "enum",
672 "examples",
673 "exclusiveMaximum",
674 "exclusiveMinimum",
675 "format",
676 "if",
677 "items",
678 "maxContains",
679 "maxItems",
680 "maxLength",
681 "maxProperties",
682 "maximum",
683 "minContains",
684 "minItems",
685 "minLength",
686 "minProperties",
687 "minimum",
688 "multipleOf",
689 "not",
690 "oneOf",
691 "pattern",
692 "patternProperties",
693 "prefixItems",
694 "properties",
695 "propertyNames",
696 "readOnly",
697 "required",
698 "then",
699 "title",
700 "type",
701 "unevaluatedItems",
702 "unevaluatedProperties",
703 "uniqueItems",
704 "writeOnly",
705 ];
706
707 for keyword in object.keys() {
708 if !SUPPORTED.contains(&keyword.as_str()) {
709 return Err(SchemaAdmissionError::new(
710 format!("{path}.{keyword}"),
711 "unsupported Draft 2020-12 vocabulary keyword",
712 ));
713 }
714 }
715 if !root && object.contains_key("$schema") && !object.contains_key("$id") {
716 return Err(SchemaAdmissionError::new(
717 format!("{path}.$schema"),
718 "nested $schema is unsupported without local resource identifiers",
719 ));
720 }
721 for keyword in ["$comment", "title", "description"] {
722 if object.get(keyword).is_some_and(|value| !value.is_string()) {
723 return Err(SchemaAdmissionError::new(
724 format!("{path}.{keyword}"),
725 "schema annotation keyword must be a string",
726 ));
727 }
728 }
729 if object
730 .get("examples")
731 .is_some_and(|value| !value.is_array())
732 {
733 return Err(SchemaAdmissionError::new(
734 format!("{path}.examples"),
735 "examples must be an array",
736 ));
737 }
738 validate_boolean_keywords(object, path, &["deprecated", "readOnly", "writeOnly"])
739}
740
741fn validate_schema_id_keyword(
742 object: &serde_json::Map<String, Value>,
743 path: &str,
744) -> Result<(), SchemaAdmissionError> {
745 let Some(identifier) = object.get("$id") else {
746 return Ok(());
747 };
748 let identifier = identifier.as_str().ok_or_else(|| {
749 SchemaAdmissionError::new(format!("{path}.$id"), "schema $id must be a string")
750 })?;
751 let (identifier, fragment) = split_uri_reference_fragment(identifier);
752 if fragment.is_some_and(|fragment| !fragment.is_empty()) {
753 return Err(SchemaAdmissionError::new(
754 format!("{path}.$id"),
755 "schema $id must not contain a fragment",
756 ));
757 }
758 if !is_bounded_uri_reference(identifier) {
759 return Err(SchemaAdmissionError::new(
760 format!("{path}.$id"),
761 "schema $id must be a bounded URI reference",
762 ));
763 }
764 Ok(())
765}
766
767fn validate_schema_dialect_keyword(
768 object: &serde_json::Map<String, Value>,
769 root_schema: &Value,
770 path: &str,
771) -> Result<(), SchemaAdmissionError> {
772 let Some(dialect) = object.get("$schema") else {
773 return Ok(());
774 };
775 let dialect = dialect.as_str().ok_or_else(|| {
776 SchemaAdmissionError::new(format!("{path}.$schema"), "unsupported schema dialect")
777 })?;
778 let dialect = AbsoluteUri::parse_with_max_bytes(dialect, MAX_SCHEMA_ASSERTION_STRING_BYTES)
779 .ok()
780 .filter(|dialect| dialect.fragment().is_none())
781 .ok_or_else(|| {
782 SchemaAdmissionError::new(
783 format!("{path}.$schema"),
784 "schema dialect must be a bounded absolute URI",
785 )
786 })?;
787 if dialect.as_str() == FINAL_JSON_SCHEMA_DIALECT {
788 return Ok(());
789 }
790 let meta_schema =
791 find_local_schema_resource(root_schema, dialect.as_str()).ok_or_else(|| {
792 SchemaAdmissionError::new(format!("{path}.$schema"), "unsupported schema dialect")
793 })?;
794 validate_meta_schema_vocabulary(meta_schema, path)?;
795 Ok(())
796}
797
798fn validate_meta_schema_vocabulary(
799 meta_schema: &Value,
800 path: &str,
801) -> Result<(), SchemaAdmissionError> {
802 let Some(vocabularies) = meta_schema
803 .as_object()
804 .and_then(|object| object.get("$vocabulary"))
805 else {
806 return Ok(());
807 };
808 let vocabularies = vocabularies.as_object().ok_or_else(|| {
809 SchemaAdmissionError::new(
810 format!("{path}.$vocabulary"),
811 "meta-schema $vocabulary must be an object",
812 )
813 })?;
814 if vocabularies.len() > MAX_SCHEMA_ASSERTION_ENTRIES {
815 return Err(SchemaAdmissionError::new(
816 format!("{path}.$vocabulary"),
817 "meta-schema $vocabulary exceeds entry limit",
818 ));
819 }
820 if vocabularies.get(CORE_VOCABULARY_URI) == Some(&Value::Bool(false)) {
821 return Err(SchemaAdmissionError::new(
822 format!("{path}.$vocabulary"),
823 "meta-schema $vocabulary cannot disable the Draft 2020-12 core vocabulary",
824 ));
825 }
826 for (vocabulary, required) in vocabularies {
827 let vocabulary_uri =
828 AbsoluteUri::parse_with_max_bytes(vocabulary, MAX_SCHEMA_ASSERTION_STRING_BYTES)
829 .map_err(|_| {
830 SchemaAdmissionError::new(
831 format!("{path}.$vocabulary"),
832 "meta-schema $vocabulary keys must be bounded absolute URIs",
833 )
834 })?;
835 if vocabulary_uri.fragment().is_some() {
836 return Err(SchemaAdmissionError::new(
837 format!("{path}.$vocabulary"),
838 "meta-schema $vocabulary keys must not contain fragments",
839 ));
840 }
841 let required = required.as_bool().ok_or_else(|| {
842 SchemaAdmissionError::new(
843 format!("{path}.$vocabulary.{vocabulary}"),
844 "meta-schema $vocabulary values must be booleans",
845 )
846 })?;
847 if required && vocabulary == FORMAT_ASSERTION_VOCABULARY_URI {
848 return Err(SchemaAdmissionError::new(
849 format!("{path}.$vocabulary.{vocabulary}"),
850 "format assertion vocabulary is not supported",
851 ));
852 }
853 if required && !is_supported_vocabulary_uri(vocabulary) {
854 return Err(SchemaAdmissionError::new(
855 format!("{path}.$vocabulary.{vocabulary}"),
856 "required schema vocabulary is not supported",
857 ));
858 }
859 }
860 if vocabularies.get(CORE_VOCABULARY_URI) != Some(&Value::Bool(true)) {
861 return Err(SchemaAdmissionError::new(
862 format!("{path}.$vocabulary"),
863 "meta-schema $vocabulary must require the Draft 2020-12 core vocabulary",
864 ));
865 }
866 Ok(())
867}
868
869fn is_supported_vocabulary_uri(vocabulary: &str) -> bool {
870 matches!(
871 vocabulary,
872 CORE_VOCABULARY_URI
873 | APPLICATOR_VOCABULARY_URI
874 | UNEVALUATED_VOCABULARY_URI
875 | VALIDATION_VOCABULARY_URI
876 | META_DATA_VOCABULARY_URI
877 | FORMAT_ANNOTATION_VOCABULARY_URI
878 | CONTENT_VOCABULARY_URI
879 )
880}
881
882fn validate_content_annotation_keywords(
883 object: &serde_json::Map<String, Value>,
884 path: &str,
885) -> Result<(), SchemaAdmissionError> {
886 for keyword in ["contentEncoding", "contentMediaType"] {
887 if object.get(keyword).is_some_and(|value| {
888 value.as_str().is_none_or(|value| {
889 value.is_empty() || value.len() > MAX_SCHEMA_ASSERTION_STRING_BYTES
890 })
891 }) {
892 return Err(SchemaAdmissionError::new(
893 format!("{path}.{keyword}"),
894 "content annotation keyword must be a bounded non-empty string",
895 ));
896 }
897 }
898 Ok(())
899}
900
901fn validate_local_reference_keyword(
902 object: &serde_json::Map<String, Value>,
903 keyword: &str,
904 root_schema: &Value,
905 path: &str,
906) -> Result<(), SchemaAdmissionError> {
907 let Some(reference) = object.get(keyword) else {
908 return Ok(());
909 };
910 let reference = reference.as_str().ok_or_else(|| {
911 SchemaAdmissionError::new(
912 format!("{path}.{keyword}"),
913 "schema reference must be a string",
914 )
915 })?;
916 match resolve_local_reference(root_schema, object, reference) {
917 Ok(target) if target.is_boolean() || target.is_object() => {
918 if !is_admitted_schema_node(root_schema, target) {
919 return Err(SchemaAdmissionError::new(
920 format!("{path}.{keyword}"),
921 "local schema reference target is not an admitted schema node",
922 ));
923 }
924 }
925 Ok(_) => {
926 return Err(SchemaAdmissionError::new(
927 format!("{path}.{keyword}"),
928 "local schema reference target must be an object or boolean",
929 ));
930 }
931 Err("external schema reference is not allowed") => {
932 return Err(SchemaAdmissionError::new(
933 format!("{path}.{keyword}"),
934 "external schema reference is not allowed",
935 ));
936 }
937 Err("invalid local schema reference") => {
938 return Err(SchemaAdmissionError::new(
939 format!("{path}.{keyword}"),
940 "invalid local schema reference",
941 ));
942 }
943 Err(_) => {
944 return Err(SchemaAdmissionError::new(
945 format!("{path}.{keyword}"),
946 "unresolved local schema reference",
947 ));
948 }
949 }
950 Ok(())
951}
952
953fn is_admitted_schema_node(schema: &Value, target: &Value) -> bool {
957 if std::ptr::eq(schema, target) {
958 return true;
959 }
960 let Some(object) = schema.as_object() else {
961 return false;
962 };
963
964 for keyword in [
965 "properties",
966 "patternProperties",
967 "$defs",
968 "dependentSchemas",
969 ] {
970 if object
971 .get(keyword)
972 .and_then(Value::as_object)
973 .is_some_and(|subschemas| {
974 subschemas
975 .values()
976 .any(|subschema| is_admitted_schema_node(subschema, target))
977 })
978 {
979 return true;
980 }
981 }
982
983 for keyword in [
984 "additionalProperties",
985 "unevaluatedProperties",
986 "unevaluatedItems",
987 "items",
988 "contains",
989 "not",
990 "if",
991 "then",
992 "else",
993 "propertyNames",
994 "contentSchema",
995 ] {
996 if object
997 .get(keyword)
998 .is_some_and(|subschema| is_admitted_schema_node(subschema, target))
999 {
1000 return true;
1001 }
1002 }
1003
1004 for keyword in ["prefixItems", "allOf", "anyOf", "oneOf"] {
1005 if object
1006 .get(keyword)
1007 .and_then(Value::as_array)
1008 .is_some_and(|subschemas| {
1009 subschemas
1010 .iter()
1011 .any(|subschema| is_admitted_schema_node(subschema, target))
1012 })
1013 {
1014 return true;
1015 }
1016 }
1017
1018 false
1019}
1020
1021fn validate_anchor_keyword(
1022 object: &serde_json::Map<String, Value>,
1023 keyword: &str,
1024 path: &str,
1025) -> Result<(), SchemaAdmissionError> {
1026 let Some(anchor) = object.get(keyword) else {
1027 return Ok(());
1028 };
1029 let anchor = anchor.as_str().ok_or_else(|| {
1030 SchemaAdmissionError::new(
1031 format!("{path}.{keyword}"),
1032 "schema anchor must be a string",
1033 )
1034 })?;
1035 if valid_anchor_name(anchor) {
1036 Ok(())
1037 } else {
1038 Err(SchemaAdmissionError::new(
1039 format!("{path}.{keyword}"),
1040 "schema anchor has an invalid name",
1041 ))
1042 }
1043}
1044
1045fn valid_anchor_name(anchor: &str) -> bool {
1046 let mut characters = anchor.chars();
1047 matches!(characters.next(), Some(character) if character.is_ascii_alphabetic() || character == '_')
1048 && characters.all(|character| {
1049 character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.')
1050 })
1051}
1052
1053fn validate_unique_local_anchors(
1054 schema: &Value,
1055 path: &str,
1056 depth: usize,
1057 anchors: &mut HashSet<String>,
1058) -> Result<(), SchemaAdmissionError> {
1059 if depth >= MAX_SCHEMA_VALIDATION_DEPTH {
1060 return Err(SchemaAdmissionError::new(
1061 path,
1062 "schema admission nesting limit exceeded",
1063 ));
1064 }
1065 let Some(object) = schema.as_object() else {
1066 return Ok(());
1067 };
1068 for keyword in ["$anchor", "$dynamicAnchor"] {
1069 if let Some(anchor) = object.get(keyword).and_then(Value::as_str)
1070 && !anchors.insert(anchor.to_owned())
1071 {
1072 return Err(SchemaAdmissionError::new(
1073 format!("{path}.{keyword}"),
1074 "duplicate local schema anchor",
1075 ));
1076 }
1077 }
1078 for keyword in [
1079 "properties",
1080 "patternProperties",
1081 "$defs",
1082 "dependentSchemas",
1083 ] {
1084 if let Some(subschemas) = object.get(keyword).and_then(Value::as_object) {
1085 for (name, subschema) in subschemas {
1086 validate_unique_local_anchor_child(
1087 subschema,
1088 &format!("{path}.{keyword}.{name}"),
1089 depth + 1,
1090 anchors,
1091 )?;
1092 }
1093 }
1094 }
1095 for keyword in [
1096 "additionalProperties",
1097 "unevaluatedProperties",
1098 "unevaluatedItems",
1099 "items",
1100 "contains",
1101 "not",
1102 "if",
1103 "then",
1104 "else",
1105 "propertyNames",
1106 "contentSchema",
1107 ] {
1108 if let Some(subschema) = object.get(keyword) {
1109 validate_unique_local_anchor_child(
1110 subschema,
1111 &format!("{path}.{keyword}"),
1112 depth + 1,
1113 anchors,
1114 )?;
1115 }
1116 }
1117 for keyword in ["prefixItems", "allOf", "anyOf", "oneOf"] {
1118 if let Some(subschemas) = object.get(keyword).and_then(Value::as_array) {
1119 for (index, subschema) in subschemas.iter().enumerate() {
1120 validate_unique_local_anchor_child(
1121 subschema,
1122 &format!("{path}.{keyword}[{index}]"),
1123 depth + 1,
1124 anchors,
1125 )?;
1126 }
1127 }
1128 }
1129 Ok(())
1130}
1131
1132fn validate_unique_local_anchor_child(
1133 schema: &Value,
1134 path: &str,
1135 depth: usize,
1136 anchors: &mut HashSet<String>,
1137) -> Result<(), SchemaAdmissionError> {
1138 if schema
1139 .as_object()
1140 .is_some_and(|object| object.contains_key("$id"))
1141 {
1142 validate_unique_local_anchors(schema, path, depth, &mut HashSet::new())
1143 } else {
1144 validate_unique_local_anchors(schema, path, depth, anchors)
1145 }
1146}
1147
1148fn validate_unique_local_resource_ids(
1149 schema: &Value,
1150 path: &str,
1151 depth: usize,
1152 parent_base: Option<&str>,
1153 identifiers: &mut HashSet<String>,
1154) -> Result<(), SchemaAdmissionError> {
1155 if depth >= MAX_SCHEMA_VALIDATION_DEPTH {
1156 return Err(SchemaAdmissionError::new(
1157 path,
1158 "schema admission nesting limit exceeded",
1159 ));
1160 }
1161 let Some(object) = schema.as_object() else {
1162 return Ok(());
1163 };
1164 let base = object
1165 .get("$id")
1166 .and_then(Value::as_str)
1167 .map(|identifier| {
1168 resolve_resource_identifier(parent_base, identifier).ok_or_else(|| {
1169 SchemaAdmissionError::new(
1170 format!("{path}.$id"),
1171 "schema $id must resolve to a bounded absolute URI",
1172 )
1173 })
1174 })
1175 .transpose()?;
1176 if let Some(identifier) = &base
1177 && !identifiers.insert(identifier.clone())
1178 {
1179 return Err(SchemaAdmissionError::new(
1180 format!("{path}.$id"),
1181 "duplicate local schema resource identifier",
1182 ));
1183 }
1184 let current_base = base.as_deref().or(parent_base);
1185 for keyword in [
1186 "properties",
1187 "patternProperties",
1188 "$defs",
1189 "dependentSchemas",
1190 ] {
1191 if let Some(subschemas) = object.get(keyword).and_then(Value::as_object) {
1192 for (name, subschema) in subschemas {
1193 validate_unique_local_resource_ids(
1194 subschema,
1195 &format!("{path}.{keyword}.{name}"),
1196 depth + 1,
1197 current_base,
1198 identifiers,
1199 )?;
1200 }
1201 }
1202 }
1203 for keyword in [
1204 "additionalProperties",
1205 "unevaluatedProperties",
1206 "unevaluatedItems",
1207 "items",
1208 "contains",
1209 "not",
1210 "if",
1211 "then",
1212 "else",
1213 "propertyNames",
1214 "contentSchema",
1215 ] {
1216 if let Some(subschema) = object.get(keyword) {
1217 validate_unique_local_resource_ids(
1218 subschema,
1219 &format!("{path}.{keyword}"),
1220 depth + 1,
1221 current_base,
1222 identifiers,
1223 )?;
1224 }
1225 }
1226 for keyword in ["prefixItems", "allOf", "anyOf", "oneOf"] {
1227 if let Some(subschemas) = object.get(keyword).and_then(Value::as_array) {
1228 for (index, subschema) in subschemas.iter().enumerate() {
1229 validate_unique_local_resource_ids(
1230 subschema,
1231 &format!("{path}.{keyword}[{index}]"),
1232 depth + 1,
1233 current_base,
1234 identifiers,
1235 )?;
1236 }
1237 }
1238 }
1239 Ok(())
1240}
1241
1242fn validate_schema_type(value: &Value, path: &str) -> Result<(), SchemaAdmissionError> {
1243 let valid_type = |type_name: &str| {
1244 matches!(
1245 type_name,
1246 "array" | "boolean" | "integer" | "null" | "number" | "object" | "string"
1247 )
1248 };
1249 match value {
1250 Value::String(type_name) if valid_type(type_name) => Ok(()),
1251 Value::Array(types)
1252 if !types.is_empty()
1253 && types
1254 .iter()
1255 .all(|type_name| type_name.as_str().is_some_and(valid_type)) =>
1256 {
1257 Ok(())
1258 }
1259 _ => Err(SchemaAdmissionError::new(
1260 path,
1261 "type must contain only JSON Schema primitive type names",
1262 )),
1263 }
1264}
1265
1266fn validate_string_array_keyword(
1267 object: &serde_json::Map<String, Value>,
1268 keyword: &str,
1269 path: &str,
1270) -> Result<(), SchemaAdmissionError> {
1271 let Some(value) = object.get(keyword) else {
1272 return Ok(());
1273 };
1274 if keyword == "dependentRequired" {
1275 let dependencies = value.as_object().ok_or_else(|| {
1276 SchemaAdmissionError::new(
1277 format!("{path}.{keyword}"),
1278 "dependentRequired must be an object",
1279 )
1280 })?;
1281 if dependencies.len() > MAX_SCHEMA_ASSERTION_ENTRIES {
1282 return Err(SchemaAdmissionError::new(
1283 format!("{path}.{keyword}"),
1284 "dependentRequired exceeds entry limit",
1285 ));
1286 }
1287 let mut validation_work = dependencies.len();
1288 for (trigger, required) in dependencies {
1289 if trigger.len() > MAX_SCHEMA_ASSERTION_STRING_BYTES {
1290 return Err(SchemaAdmissionError::new(
1291 format!("{path}.{keyword}"),
1292 "dependentRequired string exceeds byte limit",
1293 ));
1294 }
1295 let members = required.as_array().ok_or_else(|| {
1296 SchemaAdmissionError::new(
1297 format!("{path}.{keyword}"),
1298 "dependentRequired values must be arrays of strings",
1299 )
1300 })?;
1301 if members.len() > MAX_SCHEMA_ASSERTION_ENTRIES {
1302 return Err(SchemaAdmissionError::new(
1303 format!("{path}.{keyword}"),
1304 "dependentRequired values exceed entry limit",
1305 ));
1306 }
1307 validation_work = validation_work.checked_add(members.len()).ok_or_else(|| {
1308 SchemaAdmissionError::new(
1309 format!("{path}.{keyword}"),
1310 "dependentRequired exceeds validation work budget",
1311 )
1312 })?;
1313 if validation_work >= MAX_SCHEMA_VALIDATION_WORK {
1314 return Err(SchemaAdmissionError::new(
1315 format!("{path}.{keyword}"),
1316 "dependentRequired exceeds validation work budget",
1317 ));
1318 }
1319 if members.iter().any(|member| {
1320 member
1321 .as_str()
1322 .is_none_or(|member| member.len() > MAX_SCHEMA_ASSERTION_STRING_BYTES)
1323 }) {
1324 return Err(SchemaAdmissionError::new(
1325 format!("{path}.{keyword}"),
1326 "dependentRequired values must be bounded strings",
1327 ));
1328 }
1329 }
1330 return Ok(());
1331 }
1332 let members = value.as_array().ok_or_else(|| {
1333 SchemaAdmissionError::new(
1334 format!("{path}.{keyword}"),
1335 "schema string-array keyword must be an array of strings",
1336 )
1337 })?;
1338 if members.len() > MAX_SCHEMA_ASSERTION_ENTRIES {
1339 return Err(SchemaAdmissionError::new(
1340 format!("{path}.{keyword}"),
1341 "required exceeds entry limit",
1342 ));
1343 }
1344 if members.iter().any(|member| {
1345 member
1346 .as_str()
1347 .is_none_or(|member| member.len() > MAX_SCHEMA_ASSERTION_STRING_BYTES)
1348 }) {
1349 return Err(SchemaAdmissionError::new(
1350 format!("{path}.{keyword}"),
1351 "schema string-array keyword must contain bounded strings",
1352 ));
1353 }
1354 Ok(())
1355}
1356
1357fn validate_nonnegative_integer_keywords(
1358 object: &serde_json::Map<String, Value>,
1359 path: &str,
1360 keywords: &[&str],
1361) -> Result<(), SchemaAdmissionError> {
1362 for keyword in keywords {
1363 if object.get(*keyword).is_some_and(|value| {
1364 ExactDecimal::from_value(value)
1365 .is_none_or(|number| number.negative || !number.is_integer())
1366 }) {
1367 return Err(SchemaAdmissionError::new(
1368 format!("{path}.{keyword}"),
1369 "schema count keyword must be a nonnegative integer",
1370 ));
1371 }
1372 }
1373 Ok(())
1374}
1375
1376fn validate_number_keywords(
1377 object: &serde_json::Map<String, Value>,
1378 path: &str,
1379 keywords: &[&str],
1380) -> Result<(), SchemaAdmissionError> {
1381 for keyword in keywords {
1382 if object
1383 .get(*keyword)
1384 .is_some_and(|value| ExactDecimal::from_value(value).is_none())
1385 {
1386 return Err(SchemaAdmissionError::new(
1387 format!("{path}.{keyword}"),
1388 "schema numeric keyword must be a bounded exact number",
1389 ));
1390 }
1391 }
1392 Ok(())
1393}
1394
1395fn validate_boolean_keywords(
1396 object: &serde_json::Map<String, Value>,
1397 path: &str,
1398 keywords: &[&str],
1399) -> Result<(), SchemaAdmissionError> {
1400 for keyword in keywords {
1401 if object
1402 .get(*keyword)
1403 .is_some_and(|value| !value.is_boolean())
1404 {
1405 return Err(SchemaAdmissionError::new(
1406 format!("{path}.{keyword}"),
1407 "schema boolean keyword must be a boolean",
1408 ));
1409 }
1410 }
1411 Ok(())
1412}
1413
1414fn validate_enum_keyword(
1415 object: &serde_json::Map<String, Value>,
1416 path: &str,
1417) -> Result<(), SchemaAdmissionError> {
1418 if let Some(value) = object.get("enum") {
1419 let values = value.as_array().ok_or_else(|| {
1420 SchemaAdmissionError::new(format!("{path}.enum"), "enum must be a nonempty array")
1421 })?;
1422 if values.is_empty() {
1423 return Err(SchemaAdmissionError::new(
1424 format!("{path}.enum"),
1425 "enum must be a nonempty array",
1426 ));
1427 }
1428 if values.len() > MAX_SCHEMA_ASSERTION_ENTRIES {
1429 return Err(SchemaAdmissionError::new(
1430 format!("{path}.enum"),
1431 "enum exceeds entry limit",
1432 ));
1433 }
1434 }
1435 Ok(())
1436}
1437
1438fn validate_exact_equality_value(
1442 value: &Value,
1443 path: &str,
1444 depth: usize,
1445 node_count: &mut usize,
1446) -> Result<(), SchemaAdmissionError> {
1447 if depth >= MAX_SCHEMA_VALIDATION_DEPTH {
1448 return Err(SchemaAdmissionError::new(
1449 path,
1450 "schema admission nesting limit exceeded",
1451 ));
1452 }
1453 *node_count += 1;
1454 if *node_count > MAX_SCHEMA_ADMISSION_NODES {
1455 return Err(SchemaAdmissionError::new(
1456 path,
1457 "schema admission node limit exceeded",
1458 ));
1459 }
1460 match value {
1461 Value::Number(number) if ExactDecimal::from_number(number).is_none() => {
1462 Err(SchemaAdmissionError::new(
1463 path,
1464 "const or enum value exceeds exact numeric equality bound",
1465 ))
1466 }
1467 Value::String(string) if string.len() > MAX_SCHEMA_ASSERTION_STRING_BYTES => Err(
1468 SchemaAdmissionError::new(path, "schema assertion string exceeds byte limit"),
1469 ),
1470 Value::Array(values) => {
1471 for (index, value) in values.iter().enumerate() {
1472 validate_exact_equality_value(
1473 value,
1474 &format!("{path}[{index}]"),
1475 depth + 1,
1476 node_count,
1477 )?;
1478 }
1479 Ok(())
1480 }
1481 Value::Object(values) => {
1482 for (name, value) in values {
1483 if name.len() > MAX_SCHEMA_ASSERTION_STRING_BYTES {
1484 return Err(SchemaAdmissionError::new(
1485 path,
1486 "schema assertion string exceeds byte limit",
1487 ));
1488 }
1489 validate_exact_equality_value(
1490 value,
1491 &format!("{path}.{name}"),
1492 depth + 1,
1493 node_count,
1494 )?;
1495 }
1496 Ok(())
1497 }
1498 _ => Ok(()),
1499 }
1500}
1501
1502fn validate_pattern_keyword(
1503 object: &serde_json::Map<String, Value>,
1504 path: &str,
1505) -> Result<(), SchemaAdmissionError> {
1506 let Some(pattern) = object.get("pattern") else {
1507 return Ok(());
1508 };
1509 let pattern = pattern.as_str().ok_or_else(|| {
1510 SchemaAdmissionError::new(format!("{path}.pattern"), "pattern must be a string")
1511 })?;
1512 if pattern.len() > MAX_PATTERN_BYTES {
1513 return Err(SchemaAdmissionError::new(
1514 format!("{path}.pattern"),
1515 "pattern exceeds byte limit",
1516 ));
1517 }
1518 if Regex::new(pattern).is_ok() {
1519 Ok(())
1520 } else {
1521 Err(SchemaAdmissionError::new(
1522 format!("{path}.pattern"),
1523 "invalid schema pattern",
1524 ))
1525 }
1526}
1527
1528fn validate_format_keyword(
1529 object: &serde_json::Map<String, Value>,
1530 path: &str,
1531) -> Result<(), SchemaAdmissionError> {
1532 if object.get("format").is_some_and(|value| !value.is_string()) {
1533 Err(SchemaAdmissionError::new(
1534 format!("{path}.format"),
1535 "format must be a string",
1536 ))
1537 } else {
1538 Ok(())
1539 }
1540}
1541
1542pub fn validate(schema: &Value, value: &Value) -> ValidationResult {
1576 validate_with_schema_features(schema, value, false)
1577}
1578
1579fn validate_admitted_final_schema(schema: &Value, value: &Value) -> ValidationResult {
1582 validate_with_schema_features(schema, value, true)
1583}
1584
1585fn validate_with_schema_features(
1586 schema: &Value,
1587 value: &Value,
1588 enforce_unevaluated_properties: bool,
1589) -> ValidationResult {
1590 let mut errors = Vec::new();
1591 let mut context = ValidationContext::new(schema, enforce_unevaluated_properties);
1592 let mut instance_nodes = 0;
1593 if !validate_instance_bounds(value, "root", 0, &mut instance_nodes, &mut errors) {
1594 return Err(errors);
1595 }
1596 if enforce_unevaluated_properties
1597 && !consume_instance_preflight_work(&mut context, instance_nodes, "root", &mut errors)
1598 {
1599 return Err(errors);
1600 }
1601 validate_internal(schema, value, "root", &mut errors, &mut context);
1602 if context.work_exhausted
1603 && !errors
1604 .iter()
1605 .any(|error| error.message == "schema validation work limit exceeded")
1606 {
1607 push_error(&mut errors, "root", "schema validation work limit exceeded");
1608 }
1609
1610 if errors.is_empty() {
1611 Ok(())
1612 } else {
1613 Err(errors)
1614 }
1615}
1616
1617pub fn validate_strict(schema: &Value, value: &Value) -> ValidationResult {
1654 let strict_schema = make_strict_schema(schema);
1656 validate(&strict_schema, value)
1657}
1658
1659fn make_strict_schema(schema: &Value) -> Value {
1661 match schema {
1662 Value::Object(obj) => {
1663 let mut new_obj = obj.clone();
1664
1665 if let Some(type_val) = obj.get("type") {
1668 let is_object_type = type_val == "object"
1669 || type_val
1670 .as_array()
1671 .is_some_and(|arr| arr.iter().any(|t| t == "object"));
1672
1673 if is_object_type && !obj.contains_key("additionalProperties") {
1674 new_obj.insert("additionalProperties".to_string(), Value::Bool(false));
1675 }
1676 }
1677
1678 for keyword in [
1679 "properties",
1680 "patternProperties",
1681 "dependentSchemas",
1682 "$defs",
1683 ] {
1684 if let Some(Value::Object(subschemas)) = obj.get(keyword) {
1685 let strict_subschemas: serde_json::Map<String, Value> = subschemas
1686 .iter()
1687 .map(|(key, subschema)| (key.clone(), make_strict_schema(subschema)))
1688 .collect();
1689 new_obj.insert(keyword.to_owned(), Value::Object(strict_subschemas));
1690 }
1691 }
1692
1693 for keyword in [
1694 "additionalProperties",
1695 "unevaluatedProperties",
1696 "unevaluatedItems",
1697 "items",
1698 "contains",
1699 "not",
1700 "if",
1701 "then",
1702 "else",
1703 "propertyNames",
1704 "contentSchema",
1705 ] {
1706 if let Some(subschema) = obj.get(keyword) {
1707 new_obj.insert(keyword.to_owned(), make_strict_schema(subschema));
1708 }
1709 }
1710
1711 for keyword in ["prefixItems", "allOf", "anyOf", "oneOf"] {
1712 if let Some(Value::Array(subschemas)) = obj.get(keyword) {
1713 let strict_subschemas = subschemas.iter().map(make_strict_schema).collect();
1714 new_obj.insert(keyword.to_owned(), Value::Array(strict_subschemas));
1715 }
1716 }
1717
1718 Value::Object(new_obj)
1719 }
1720 Value::Array(arr) => {
1721 Value::Array(arr.iter().map(make_strict_schema).collect())
1723 }
1724 _ => schema.clone(),
1725 }
1726}
1727
1728struct ValidationContext<'a> {
1729 root_schema: &'a Value,
1730 schema_depth: usize,
1731 reference_depth: usize,
1732 dynamic_anchors: Vec<(String, Value)>,
1733 remaining_work: usize,
1734 work_exhausted: bool,
1735 enforce_unevaluated_properties: bool,
1736}
1737
1738impl<'a> ValidationContext<'a> {
1739 const fn new(root_schema: &'a Value, enforce_unevaluated_properties: bool) -> Self {
1740 Self {
1741 root_schema,
1742 schema_depth: 0,
1743 reference_depth: 0,
1744 dynamic_anchors: Vec::new(),
1745 remaining_work: MAX_SCHEMA_VALIDATION_WORK,
1746 work_exhausted: false,
1747 enforce_unevaluated_properties,
1748 }
1749 }
1750
1751 fn consume_work(&mut self) -> bool {
1752 let Some(remaining_work) = self.remaining_work.checked_sub(1) else {
1753 self.work_exhausted = true;
1754 return false;
1755 };
1756 self.remaining_work = remaining_work;
1757 true
1758 }
1759
1760 fn enter_schema(&mut self) -> bool {
1761 if self.schema_depth >= MAX_SCHEMA_VALIDATION_DEPTH || !self.consume_work() {
1762 return false;
1763 }
1764 self.schema_depth += 1;
1765 true
1766 }
1767
1768 fn leave_schema(&mut self) {
1769 self.schema_depth -= 1;
1770 }
1771
1772 fn enter_reference(&mut self) -> bool {
1773 if self.reference_depth >= MAX_LOCAL_REFERENCE_DEPTH {
1774 return false;
1775 }
1776 self.reference_depth += 1;
1777 true
1778 }
1779
1780 fn leave_reference(&mut self) {
1781 self.reference_depth -= 1;
1782 }
1783
1784 fn push_dynamic_anchor(&mut self, name: &str, schema: &Value) {
1785 self.dynamic_anchors.push((name.to_owned(), schema.clone()));
1786 }
1787
1788 fn pop_dynamic_anchor(&mut self) {
1789 let _ = self.dynamic_anchors.pop();
1790 }
1791
1792 fn dynamic_anchor_target(&self, name: &str) -> Option<Value> {
1793 self.dynamic_anchors
1794 .iter()
1795 .rev()
1796 .find(|(candidate, _)| candidate == name)
1797 .map(|(_, schema)| schema.clone())
1798 }
1799}
1800
1801fn consume_instance_preflight_work(
1802 context: &mut ValidationContext<'_>,
1803 nodes: usize,
1804 path: &str,
1805 errors: &mut Vec<ValidationError>,
1806) -> bool {
1807 for _ in 0..nodes {
1808 if !context.consume_work() {
1809 push_error(errors, path, "schema validation work limit exceeded");
1810 return false;
1811 }
1812 }
1813 true
1814}
1815
1816fn consume_validation_work(
1817 context: &mut ValidationContext<'_>,
1818 path: &str,
1819 errors: &mut Vec<ValidationError>,
1820) -> bool {
1821 if !context.enforce_unevaluated_properties {
1822 return true;
1823 }
1824 if context.consume_work() {
1825 true
1826 } else {
1827 push_error(errors, path, "schema validation work limit exceeded");
1828 false
1829 }
1830}
1831
1832fn charged_regex_is_match(
1833 pattern: &Regex,
1834 candidate: &str,
1835 path: &str,
1836 errors: &mut Vec<ValidationError>,
1837 context: &mut ValidationContext<'_>,
1838) -> Option<bool> {
1839 consume_validation_work(context, path, errors).then(|| pattern.is_match(candidate))
1840}
1841
1842fn validate_instance_bounds(
1843 value: &Value,
1844 path: &str,
1845 depth: usize,
1846 node_count: &mut usize,
1847 errors: &mut Vec<ValidationError>,
1848) -> bool {
1849 if depth >= MAX_SCHEMA_INSTANCE_DEPTH {
1850 push_error(errors, path, "instance nesting limit exceeded");
1851 return false;
1852 }
1853 *node_count += 1;
1854 if *node_count > MAX_SCHEMA_INSTANCE_NODES {
1855 push_error(errors, path, "instance node limit exceeded");
1856 return false;
1857 }
1858
1859 match value {
1860 Value::String(string) => {
1861 if string.len() > MAX_SCHEMA_INSTANCE_STRING_BYTES {
1862 push_error(errors, path, "instance string byte limit exceeded");
1863 return false;
1864 }
1865 }
1866 Value::Array(items) => {
1867 for (index, item) in items.iter().enumerate() {
1868 if !validate_instance_bounds(
1869 item,
1870 &format!("{path}[{index}]"),
1871 depth + 1,
1872 node_count,
1873 errors,
1874 ) {
1875 return false;
1876 }
1877 }
1878 }
1879 Value::Object(members) => {
1880 for (name, member) in members {
1881 if name.len() > MAX_SCHEMA_INSTANCE_STRING_BYTES {
1882 push_error(
1883 errors,
1884 path,
1885 "instance object member-name byte limit exceeded",
1886 );
1887 return false;
1888 }
1889 if !validate_instance_bounds(
1890 member,
1891 &format!("{path}.{name}"),
1892 depth + 1,
1893 node_count,
1894 errors,
1895 ) {
1896 return false;
1897 }
1898 }
1899 }
1900 Value::Null | Value::Bool(_) | Value::Number(_) => {}
1901 }
1902 true
1903}
1904
1905fn push_error(errors: &mut Vec<ValidationError>, path: &str, message: impl Into<String>) {
1906 if errors.len() < MAX_VALIDATION_ERRORS {
1907 errors.push(ValidationError {
1908 path: path.to_owned(),
1909 message: message.into(),
1910 });
1911 }
1912}
1913
1914fn validate_internal(
1916 schema: &Value,
1917 value: &Value,
1918 path: &str,
1919 errors: &mut Vec<ValidationError>,
1920 context: &mut ValidationContext<'_>,
1921) {
1922 if !context.enter_schema() {
1923 let reason = if context.remaining_work == 0 {
1924 "schema validation work limit exceeded"
1925 } else {
1926 "schema validation nesting limit exceeded"
1927 };
1928 push_error(errors, path, reason);
1929 return;
1930 }
1931
1932 if let Some(b) = schema.as_bool() {
1934 if !b {
1935 push_error(errors, path, "schema rejects all values");
1936 }
1937 context.leave_schema();
1938 return;
1939 }
1940
1941 let Some(schema_obj) = schema.as_object() else {
1943 context.leave_schema();
1944 return; };
1946
1947 if let Some(type_val) = schema_obj.get("type") {
1949 if !validate_type(type_val, value, context.enforce_unevaluated_properties) {
1950 let expected = type_val
1951 .as_str()
1952 .map(String::from)
1953 .or_else(|| type_val.as_array().map(|arr| format!("{arr:?}")))
1954 .unwrap_or_else(|| "unknown".to_string());
1955 push_error(
1956 errors,
1957 path,
1958 format!(
1959 "expected type {expected}, got {}",
1960 json_type_name_with_final_semantics(
1961 value,
1962 context.enforce_unevaluated_properties
1963 )
1964 ),
1965 );
1966 context.leave_schema();
1967 return; }
1969 }
1970
1971 let has_dynamic_anchor = context.enforce_unevaluated_properties
1972 && schema_obj
1973 .get("$dynamicAnchor")
1974 .and_then(Value::as_str)
1975 .map(|name| {
1976 context.push_dynamic_anchor(name, schema);
1977 })
1978 .is_some();
1979
1980 validate_local_reference(schema_obj, value, path, errors, context);
1981 validate_dynamic_reference(schema_obj, value, path, errors, context);
1982 validate_composition(schema_obj, value, path, errors, context);
1983
1984 if let Some(enum_val) = schema_obj.get("enum") {
1986 if let Some(enum_arr) = enum_val.as_array() {
1987 let matches = if context.enforce_unevaluated_properties {
1988 let mut matches = false;
1989 for candidate in enum_arr {
1990 match json_schema_equal_with_work(candidate, value, path, errors, context) {
1991 Some(true) => {
1992 matches = true;
1993 break;
1994 }
1995 Some(false) => {}
1996 None => break,
1997 }
1998 }
1999 matches
2000 } else {
2001 enum_arr.contains(value)
2002 };
2003 if !matches && !context.work_exhausted {
2004 push_error(errors, path, format!("value must be one of: {enum_arr:?}"));
2005 }
2006 }
2007 }
2008
2009 if let Some(const_val) = schema_obj.get("const") {
2011 let matches = if context.enforce_unevaluated_properties {
2012 json_schema_equal_with_work(value, const_val, path, errors, context)
2013 } else {
2014 Some(value == const_val)
2015 };
2016 if matches == Some(false) {
2017 push_error(errors, path, format!("value must equal {const_val}"));
2018 }
2019 }
2020
2021 match value {
2023 Value::Object(obj) => {
2024 validate_object(schema_obj, value, obj, path, errors, context);
2025 }
2026 Value::Array(arr) => {
2027 validate_array(schema_obj, arr, path, errors, context);
2028 }
2029 Value::String(s) => {
2030 validate_string(schema_obj, s, path, errors, context);
2031 }
2032 Value::Number(n) => {
2033 validate_number(
2034 schema_obj,
2035 n,
2036 path,
2037 errors,
2038 context.enforce_unevaluated_properties,
2039 context,
2040 );
2041 }
2042 _ => {}
2043 }
2044 if has_dynamic_anchor {
2045 context.pop_dynamic_anchor();
2046 }
2047 context.leave_schema();
2048}
2049
2050fn validate_local_reference(
2051 schema: &serde_json::Map<String, Value>,
2052 value: &Value,
2053 path: &str,
2054 errors: &mut Vec<ValidationError>,
2055 context: &mut ValidationContext<'_>,
2056) {
2057 let Some(reference) = schema.get("$ref").and_then(Value::as_str) else {
2058 return;
2059 };
2060 if !context.enter_reference() {
2061 push_error(errors, path, "local schema reference depth limit exceeded");
2062 return;
2063 }
2064 let root_schema = context.root_schema;
2065 let resolution = if context.enforce_unevaluated_properties {
2066 resolve_local_reference_with_work(root_schema, schema, reference, context)
2067 } else {
2068 resolve_legacy_local_reference(root_schema, reference)
2069 };
2070 match resolution {
2071 Ok(target) => validate_internal(target, value, path, errors, context),
2072 Err(message) => push_error(errors, path, message),
2073 }
2074 context.leave_reference();
2075}
2076
2077fn validate_dynamic_reference(
2078 schema: &serde_json::Map<String, Value>,
2079 value: &Value,
2080 path: &str,
2081 errors: &mut Vec<ValidationError>,
2082 context: &mut ValidationContext<'_>,
2083) {
2084 if !context.enforce_unevaluated_properties {
2085 return;
2086 }
2087 let Some(reference) = schema.get("$dynamicRef").and_then(Value::as_str) else {
2088 return;
2089 };
2090 if !context.enter_reference() {
2091 push_error(errors, path, "local schema reference depth limit exceeded");
2092 return;
2093 }
2094 let root_schema = context.root_schema;
2095 match resolve_local_reference_with_work(root_schema, schema, reference, context) {
2096 Ok(target) => {
2097 let dynamic_anchor = target
2098 .as_object()
2099 .and_then(|object| object.get("$dynamicAnchor"))
2100 .and_then(Value::as_str);
2101 let selected = dynamic_anchor
2102 .and_then(|name| context.dynamic_anchor_target(name))
2103 .unwrap_or_else(|| target.clone());
2104 validate_internal(&selected, value, path, errors, context);
2105 }
2106 Err(message) => push_error(errors, path, message),
2107 }
2108 context.leave_reference();
2109}
2110
2111struct SchemaResourceScope<'a> {
2112 resource: &'a Value,
2113 base: Option<String>,
2114}
2115
2116fn resolve_local_reference<'a>(
2117 root_schema: &'a Value,
2118 source: &serde_json::Map<String, Value>,
2119 reference: &str,
2120) -> Result<&'a Value, &'static str> {
2121 let mut uncharged = || Ok(());
2122 resolve_local_reference_with_charge(root_schema, source, reference, &mut uncharged)
2123}
2124
2125fn resolve_local_reference_with_work<'a>(
2126 root_schema: &'a Value,
2127 source: &serde_json::Map<String, Value>,
2128 reference: &str,
2129 context: &mut ValidationContext<'_>,
2130) -> Result<&'a Value, &'static str> {
2131 let mut charge = || {
2132 if context.consume_work() {
2133 Ok(())
2134 } else {
2135 Err("schema validation work limit exceeded")
2136 }
2137 };
2138 resolve_local_reference_with_charge(root_schema, source, reference, &mut charge)
2139}
2140
2141fn resolve_local_reference_with_charge<'a>(
2142 root_schema: &'a Value,
2143 source: &serde_json::Map<String, Value>,
2144 reference: &str,
2145 charge: &mut dyn FnMut() -> Result<(), &'static str>,
2146) -> Result<&'a Value, &'static str> {
2147 if reference.len() > MAX_SCHEMA_ASSERTION_STRING_BYTES {
2148 return Err("schema reference exceeds byte limit");
2149 }
2150 if !is_bounded_uri_reference(reference) {
2151 return Err("invalid local schema reference");
2152 }
2153 let source_scope = find_schema_resource_scope(root_schema, source, charge)?
2154 .ok_or("unresolved local schema reference")?;
2155 let (identifier, fragment) = split_uri_reference_fragment(reference);
2156 let fragment = match fragment {
2157 Some(fragment) => {
2158 Some(decode_uri_fragment(fragment).ok_or("invalid local schema reference")?)
2159 }
2160 None => None,
2161 };
2162 let resource = if identifier.is_empty() {
2163 source_scope.resource
2164 } else {
2165 let identifier = resolve_uri_reference(source_scope.base.as_deref(), identifier)
2166 .ok_or("external schema reference is not allowed")?;
2167 find_local_schema_resource_with_charge(root_schema, &identifier, charge)?
2168 .ok_or("external schema reference is not allowed")?
2169 };
2170 resolve_local_reference_fragment_with_charge(
2171 resource,
2172 fragment.as_deref().unwrap_or(""),
2173 charge,
2174 )
2175}
2176
2177fn resolve_local_reference_fragment<'a>(
2178 resource: &'a Value,
2179 fragment: &str,
2180) -> Result<&'a Value, &'static str> {
2181 if !fragment.is_empty() && !fragment.starts_with('/') {
2182 return find_resource_anchor(resource, fragment).ok_or("unresolved local schema reference");
2183 }
2184 let mut uncharged = || Ok(());
2185 resolve_local_reference_fragment_with_charge(resource, fragment, &mut uncharged)
2186}
2187
2188fn resolve_local_reference_fragment_with_charge<'a>(
2189 resource: &'a Value,
2190 fragment: &str,
2191 charge: &mut dyn FnMut() -> Result<(), &'static str>,
2192) -> Result<&'a Value, &'static str> {
2193 if fragment.is_empty() {
2194 return Ok(resource);
2195 }
2196 if let Some(pointer) = fragment.strip_prefix('/') {
2197 let mut target = resource;
2198 for encoded_segment in pointer.split('/') {
2199 charge()?;
2200 let segment = unescape_json_pointer_segment(encoded_segment)
2201 .ok_or("invalid local schema reference")?;
2202 target = match target {
2203 Value::Object(object) => object.get(&segment),
2204 Value::Array(array) => segment
2205 .parse::<usize>()
2206 .ok()
2207 .and_then(|index| array.get(index)),
2208 _ => None,
2209 }
2210 .ok_or("unresolved local schema reference")?;
2211 }
2212 return Ok(target);
2213 }
2214 find_resource_anchor_with_charge(resource, fragment, charge)?
2215 .ok_or("unresolved local schema reference")
2216}
2217
2218fn resolve_legacy_local_reference<'a>(
2221 root_schema: &'a Value,
2222 reference: &str,
2223) -> Result<&'a Value, &'static str> {
2224 if reference == "#" || reference.starts_with("#/") {
2225 resolve_local_reference_fragment(root_schema, &reference[1..])
2226 } else {
2227 Err("external schema reference is not allowed")
2228 }
2229}
2230
2231fn find_schema_resource_scope<'a>(
2232 root_schema: &'a Value,
2233 target: &serde_json::Map<String, Value>,
2234 charge: &mut dyn FnMut() -> Result<(), &'static str>,
2235) -> Result<Option<SchemaResourceScope<'a>>, &'static str> {
2236 find_schema_resource_scope_inner(root_schema, target, None, root_schema, None, charge)
2237}
2238
2239fn find_schema_resource_scope_inner<'a>(
2240 schema: &'a Value,
2241 target: &serde_json::Map<String, Value>,
2242 parent_base: Option<&str>,
2243 inherited_resource: &'a Value,
2244 inherited_base: Option<String>,
2245 charge: &mut dyn FnMut() -> Result<(), &'static str>,
2246) -> Result<Option<SchemaResourceScope<'a>>, &'static str> {
2247 charge()?;
2248 let Some(object) = schema.as_object() else {
2249 return Ok(None);
2250 };
2251 let identifier = object.get("$id").and_then(Value::as_str);
2252 let base = if let Some(identifier) = identifier {
2253 Some(
2254 resolve_resource_identifier(parent_base, identifier)
2255 .ok_or("unresolved local schema reference")?,
2256 )
2257 } else {
2258 inherited_base
2259 };
2260 let resource = if identifier.is_some() {
2261 schema
2262 } else {
2263 inherited_resource
2264 };
2265 if std::ptr::eq(object, target) {
2266 return Ok(Some(SchemaResourceScope { resource, base }));
2267 }
2268 find_schema_child_scope(
2269 schema,
2270 target,
2271 base.as_deref(),
2272 resource,
2273 base.clone(),
2274 charge,
2275 )
2276}
2277
2278fn find_schema_child_scope<'a>(
2279 schema: &'a Value,
2280 target: &serde_json::Map<String, Value>,
2281 base: Option<&str>,
2282 resource: &'a Value,
2283 inherited_base: Option<String>,
2284 charge: &mut dyn FnMut() -> Result<(), &'static str>,
2285) -> Result<Option<SchemaResourceScope<'a>>, &'static str> {
2286 let Some(object) = schema.as_object() else {
2287 return Ok(None);
2288 };
2289 for keyword in [
2290 "properties",
2291 "patternProperties",
2292 "$defs",
2293 "dependentSchemas",
2294 ] {
2295 if let Some(subschemas) = object.get(keyword).and_then(Value::as_object) {
2296 for subschema in subschemas.values() {
2297 if let Some(scope) = find_schema_resource_scope_inner(
2298 subschema,
2299 target,
2300 base,
2301 resource,
2302 inherited_base.clone(),
2303 charge,
2304 )? {
2305 return Ok(Some(scope));
2306 }
2307 }
2308 }
2309 }
2310 for keyword in [
2311 "additionalProperties",
2312 "unevaluatedProperties",
2313 "unevaluatedItems",
2314 "items",
2315 "contains",
2316 "not",
2317 "if",
2318 "then",
2319 "else",
2320 "propertyNames",
2321 "contentSchema",
2322 ] {
2323 if let Some(subschema) = object.get(keyword)
2324 && let Some(scope) = find_schema_resource_scope_inner(
2325 subschema,
2326 target,
2327 base,
2328 resource,
2329 inherited_base.clone(),
2330 charge,
2331 )?
2332 {
2333 return Ok(Some(scope));
2334 }
2335 }
2336 for keyword in ["prefixItems", "allOf", "anyOf", "oneOf"] {
2337 if let Some(subschemas) = object.get(keyword).and_then(Value::as_array) {
2338 for subschema in subschemas {
2339 if let Some(scope) = find_schema_resource_scope_inner(
2340 subschema,
2341 target,
2342 base,
2343 resource,
2344 inherited_base.clone(),
2345 charge,
2346 )? {
2347 return Ok(Some(scope));
2348 }
2349 }
2350 }
2351 }
2352 Ok(None)
2353}
2354
2355fn find_local_schema_resource<'a>(schema: &'a Value, identifier: &str) -> Option<&'a Value> {
2356 let mut uncharged = || Ok(());
2357 find_local_schema_resource_with_charge(schema, identifier, &mut uncharged)
2358 .ok()
2359 .flatten()
2360}
2361
2362fn find_local_schema_resource_with_charge<'a>(
2363 schema: &'a Value,
2364 identifier: &str,
2365 charge: &mut dyn FnMut() -> Result<(), &'static str>,
2366) -> Result<Option<&'a Value>, &'static str> {
2367 find_local_schema_resource_inner(schema, identifier, None, charge)
2368}
2369
2370fn find_local_schema_resource_inner<'a>(
2371 schema: &'a Value,
2372 identifier: &str,
2373 parent_base: Option<&str>,
2374 charge: &mut dyn FnMut() -> Result<(), &'static str>,
2375) -> Result<Option<&'a Value>, &'static str> {
2376 charge()?;
2377 let Some(object) = schema.as_object() else {
2378 return Ok(None);
2379 };
2380 let base = if let Some(candidate) = object.get("$id").and_then(Value::as_str) {
2381 Some(
2382 resolve_resource_identifier(parent_base, candidate)
2383 .ok_or("unresolved local schema reference")?,
2384 )
2385 } else {
2386 None
2387 };
2388 if base.as_deref() == Some(identifier) {
2389 return Ok(Some(schema));
2390 }
2391 let inherited_base = base.as_deref().or(parent_base);
2392 find_local_schema_resource_child(schema, identifier, inherited_base, charge)
2393}
2394
2395fn find_local_schema_resource_child<'a>(
2396 schema: &'a Value,
2397 identifier: &str,
2398 parent_base: Option<&str>,
2399 charge: &mut dyn FnMut() -> Result<(), &'static str>,
2400) -> Result<Option<&'a Value>, &'static str> {
2401 let Some(object) = schema.as_object() else {
2402 return Ok(None);
2403 };
2404 for keyword in [
2405 "properties",
2406 "patternProperties",
2407 "$defs",
2408 "dependentSchemas",
2409 ] {
2410 if let Some(subschemas) = object.get(keyword).and_then(Value::as_object) {
2411 for subschema in subschemas.values() {
2412 if let Some(found) =
2413 find_local_schema_resource_inner(subschema, identifier, parent_base, charge)?
2414 {
2415 return Ok(Some(found));
2416 }
2417 }
2418 }
2419 }
2420 for keyword in [
2421 "additionalProperties",
2422 "unevaluatedProperties",
2423 "unevaluatedItems",
2424 "items",
2425 "contains",
2426 "not",
2427 "if",
2428 "then",
2429 "else",
2430 "propertyNames",
2431 "contentSchema",
2432 ] {
2433 if let Some(subschema) = object.get(keyword)
2434 && let Some(found) =
2435 find_local_schema_resource_inner(subschema, identifier, parent_base, charge)?
2436 {
2437 return Ok(Some(found));
2438 }
2439 }
2440 for keyword in ["prefixItems", "allOf", "anyOf", "oneOf"] {
2441 if let Some(subschemas) = object.get(keyword).and_then(Value::as_array) {
2442 for subschema in subschemas {
2443 if let Some(found) =
2444 find_local_schema_resource_inner(subschema, identifier, parent_base, charge)?
2445 {
2446 return Ok(Some(found));
2447 }
2448 }
2449 }
2450 }
2451 Ok(None)
2452}
2453
2454fn split_uri_reference_fragment(reference: &str) -> (&str, Option<&str>) {
2455 reference
2456 .split_once('#')
2457 .map_or((reference, None), |(identifier, fragment)| {
2458 (identifier, Some(fragment))
2459 })
2460}
2461
2462fn decode_uri_fragment(fragment: &str) -> Option<String> {
2463 let mut decoded = Vec::with_capacity(fragment.len());
2464 let bytes = fragment.as_bytes();
2465 let mut index = 0;
2466 while index < bytes.len() {
2467 if bytes[index] != b'%' {
2468 decoded.push(bytes[index]);
2469 index += 1;
2470 continue;
2471 }
2472 let high = *bytes.get(index + 1)?;
2473 let low = *bytes.get(index + 2)?;
2474 decoded.push((hex_value(high)? << 4) | hex_value(low)?);
2475 index += 3;
2476 }
2477 String::from_utf8(decoded).ok()
2478}
2479
2480fn hex_value(byte: u8) -> Option<u8> {
2481 match byte {
2482 b'0'..=b'9' => Some(byte - b'0'),
2483 b'a'..=b'f' => Some(byte - b'a' + 10),
2484 b'A'..=b'F' => Some(byte - b'A' + 10),
2485 _ => None,
2486 }
2487}
2488
2489fn resolve_resource_identifier(parent_base: Option<&str>, identifier: &str) -> Option<String> {
2490 let (identifier, fragment) = split_uri_reference_fragment(identifier);
2491 if fragment.is_some_and(|fragment| !fragment.is_empty()) {
2492 return None;
2493 }
2494 resolve_uri_reference(parent_base, identifier)
2495}
2496
2497fn resolve_uri_reference(base: Option<&str>, reference: &str) -> Option<String> {
2498 if !is_bounded_uri_reference(reference) {
2499 return None;
2500 }
2501 if has_uri_scheme(reference) {
2502 return normalize_absolute_uri_reference(reference);
2503 }
2504 let base = base?;
2505 let base_uri =
2506 AbsoluteUri::parse_with_max_bytes(base, MAX_SCHEMA_ASSERTION_STRING_BYTES).ok()?;
2507 let scheme = base_uri.scheme().as_str();
2508 let (base_without_query, base_query) = base
2509 .split_once('?')
2510 .map_or((base, None), |(value, query)| (value, Some(query)));
2511 let (reference_path, reference_query) = reference
2512 .split_once('?')
2513 .map_or((reference, None), |(path, query)| (path, Some(query)));
2514 if reference_path.starts_with("//") {
2515 let resolved = format!("{scheme}:{reference}");
2516 return normalize_absolute_uri_reference(&resolved);
2517 }
2518 let hierarchy = base_without_query
2519 .strip_prefix(&format!("{scheme}:"))
2520 .filter(|value| value.starts_with("//"))?;
2521 let authority_end = hierarchy[2..]
2522 .find('/')
2523 .map_or(hierarchy.len(), |index| index + 2);
2524 let origin = format!("{scheme}:{}", &hierarchy[..authority_end]);
2525 let base_path = &hierarchy[authority_end..];
2526 let path = if reference_path.is_empty() {
2527 base_path.to_owned()
2528 } else if reference_path.starts_with('/') {
2529 remove_uri_dot_segments(reference_path)
2530 } else {
2531 let directory_end = base_path.rfind('/').map_or(0, |index| index + 1);
2532 let directory = if base_path.is_empty() {
2533 "/"
2534 } else {
2535 &base_path[..directory_end]
2536 };
2537 remove_uri_dot_segments(&format!("{}{}", directory, reference_path))
2538 };
2539 let query = match (reference_path.is_empty(), reference_query, base_query) {
2543 (_, Some(query), _) => format!("?{query}"),
2544 (true, None, Some(query)) => format!("?{query}"),
2545 (true, None, None) | (false, None, _) => String::new(),
2546 };
2547 let resolved = format!("{origin}{path}{query}");
2548 AbsoluteUri::parse_with_max_bytes(&resolved, MAX_SCHEMA_ASSERTION_STRING_BYTES)
2549 .ok()
2550 .map(|uri| uri.as_str().to_owned())
2551}
2552
2553fn normalize_absolute_uri_reference(reference: &str) -> Option<String> {
2554 let uri = AbsoluteUri::parse_with_max_bytes(reference, MAX_SCHEMA_ASSERTION_STRING_BYTES)
2555 .ok()
2556 .filter(|uri| uri.fragment().is_none())?;
2557 let scheme = uri.scheme().as_str();
2558 let hierarchy = reference.strip_prefix(&format!("{scheme}:"))?;
2559 let (hierarchy, query) = hierarchy
2560 .split_once('?')
2561 .map_or((hierarchy, None), |(path, query)| (path, Some(query)));
2562 let normalized = if let Some(authority_and_path) = hierarchy.strip_prefix("//") {
2563 let authority_end = authority_and_path
2564 .find('/')
2565 .map_or(hierarchy.len(), |index| index + 2);
2566 let authority = &hierarchy[..authority_end];
2567 let path = remove_uri_dot_segments(&hierarchy[authority_end..]);
2568 format!("{authority}{path}")
2569 } else {
2570 remove_uri_dot_segments(hierarchy)
2571 };
2572 let query = query.map_or_else(String::new, |query| format!("?{query}"));
2573 let normalized = format!("{scheme}:{normalized}{query}");
2574 AbsoluteUri::parse_with_max_bytes(&normalized, MAX_SCHEMA_ASSERTION_STRING_BYTES)
2575 .ok()
2576 .filter(|uri| uri.fragment().is_none())
2577 .map(|uri| uri.as_str().to_owned())
2578}
2579
2580fn has_uri_scheme(reference: &str) -> bool {
2581 reference.find(':').is_some_and(|index| {
2582 !reference[..index].contains(['/', '?']) && is_valid_uri_scheme(&reference[..index])
2583 })
2584}
2585
2586fn is_bounded_uri_reference(reference: &str) -> bool {
2587 reference.len() <= MAX_SCHEMA_ASSERTION_STRING_BYTES
2588 && reference.bytes().enumerate().all(|(index, byte)| {
2589 if byte == b'%' {
2590 return reference
2591 .as_bytes()
2592 .get(index + 1..=index + 2)
2593 .is_some_and(|digits| digits.iter().all(u8::is_ascii_hexdigit));
2594 }
2595 byte.is_ascii()
2596 && byte > b' '
2597 && byte != 0x7f
2598 && (byte.is_ascii_alphanumeric()
2599 || matches!(
2600 byte,
2601 b'-' | b'.'
2602 | b'_'
2603 | b'~'
2604 | b':'
2605 | b'/'
2606 | b'?'
2607 | b'#'
2608 | b'['
2609 | b']'
2610 | b'@'
2611 | b'!'
2612 | b'$'
2613 | b'&'
2614 | b'\''
2615 | b'('
2616 | b')'
2617 | b'*'
2618 | b'+'
2619 | b','
2620 | b';'
2621 | b'='
2622 | b'%'
2623 ))
2624 })
2625}
2626
2627fn remove_uri_dot_segments(path: &str) -> String {
2633 let mut input = path.to_owned();
2634 let mut output = String::with_capacity(path.len());
2635
2636 while !input.is_empty() {
2637 if input.starts_with("../") {
2638 input.drain(..3);
2639 } else if input.starts_with("./") {
2640 input.drain(..2);
2641 } else if input.starts_with("/./") {
2642 input.replace_range(..3, "/");
2643 } else if input == "/." {
2644 input.truncate(1);
2645 } else if input.starts_with("/../") {
2646 input.replace_range(..4, "/");
2647 remove_last_uri_path_segment(&mut output);
2648 } else if input == "/.." {
2649 input.replace_range(..3, "/");
2650 remove_last_uri_path_segment(&mut output);
2651 } else if input == "." || input == ".." {
2652 input.clear();
2653 } else {
2654 let segment_end = if let Some(path_after_initial_slash) = input.strip_prefix('/') {
2657 path_after_initial_slash
2658 .find('/')
2659 .map_or(input.len(), |index| index + 1)
2660 } else {
2661 input.find('/').unwrap_or(input.len())
2662 };
2663 output.push_str(&input[..segment_end]);
2664 input.drain(..segment_end);
2665 }
2666 }
2667
2668 output
2669}
2670
2671fn remove_last_uri_path_segment(output: &mut String) {
2673 if let Some(separator) = output.rfind('/') {
2674 output.truncate(separator);
2675 } else {
2676 output.clear();
2677 }
2678}
2679
2680fn find_resource_anchor<'a>(schema: &'a Value, anchor: &str) -> Option<&'a Value> {
2681 let mut uncharged = || Ok(());
2682 find_resource_anchor_with_charge(schema, anchor, &mut uncharged)
2683 .ok()
2684 .flatten()
2685}
2686
2687fn find_resource_anchor_with_charge<'a>(
2688 schema: &'a Value,
2689 anchor: &str,
2690 charge: &mut dyn FnMut() -> Result<(), &'static str>,
2691) -> Result<Option<&'a Value>, &'static str> {
2692 find_resource_anchor_inner(schema, anchor, true, charge)
2693}
2694
2695fn find_resource_anchor_inner<'a>(
2696 schema: &'a Value,
2697 anchor: &str,
2698 resource_root: bool,
2699 charge: &mut dyn FnMut() -> Result<(), &'static str>,
2700) -> Result<Option<&'a Value>, &'static str> {
2701 charge()?;
2702 let Some(object) = schema.as_object() else {
2703 return Ok(None);
2704 };
2705 if !resource_root && object.contains_key("$id") {
2706 return Ok(None);
2707 }
2708 if ["$anchor", "$dynamicAnchor"].iter().any(|keyword| {
2709 object
2710 .get(*keyword)
2711 .and_then(Value::as_str)
2712 .is_some_and(|candidate| candidate == anchor)
2713 }) {
2714 return Ok(Some(schema));
2715 }
2716 for keyword in [
2717 "properties",
2718 "patternProperties",
2719 "$defs",
2720 "dependentSchemas",
2721 ] {
2722 if let Some(subschemas) = object.get(keyword).and_then(Value::as_object) {
2723 for subschema in subschemas.values() {
2724 if let Some(found) = find_resource_anchor_inner(subschema, anchor, false, charge)? {
2725 return Ok(Some(found));
2726 }
2727 }
2728 }
2729 }
2730 for keyword in [
2731 "additionalProperties",
2732 "unevaluatedProperties",
2733 "unevaluatedItems",
2734 "items",
2735 "contains",
2736 "not",
2737 "if",
2738 "then",
2739 "else",
2740 "propertyNames",
2741 "contentSchema",
2742 ] {
2743 if let Some(subschema) = object.get(keyword)
2744 && let Some(found) = find_resource_anchor_inner(subschema, anchor, false, charge)?
2745 {
2746 return Ok(Some(found));
2747 }
2748 }
2749 for keyword in ["prefixItems", "allOf", "anyOf", "oneOf"] {
2750 if let Some(subschemas) = object.get(keyword).and_then(Value::as_array) {
2751 for subschema in subschemas {
2752 if let Some(found) = find_resource_anchor_inner(subschema, anchor, false, charge)? {
2753 return Ok(Some(found));
2754 }
2755 }
2756 }
2757 }
2758 Ok(None)
2759}
2760
2761fn unescape_json_pointer_segment(segment: &str) -> Option<String> {
2762 let mut decoded = String::with_capacity(segment.len());
2763 let mut characters = segment.chars();
2764 while let Some(character) = characters.next() {
2765 if character != '~' {
2766 decoded.push(character);
2767 continue;
2768 }
2769 match characters.next()? {
2770 '0' => decoded.push('~'),
2771 '1' => decoded.push('/'),
2772 _ => return None,
2773 }
2774 }
2775 Some(decoded)
2776}
2777
2778fn validate_composition(
2779 schema: &serde_json::Map<String, Value>,
2780 value: &Value,
2781 path: &str,
2782 errors: &mut Vec<ValidationError>,
2783 context: &mut ValidationContext<'_>,
2784) {
2785 validate_all_of(schema, value, path, errors, context);
2786 validate_any_of(schema, value, path, errors, context);
2787 validate_one_of(schema, value, path, errors, context);
2788 validate_not(schema, value, path, errors, context);
2789 validate_conditional(schema, value, path, errors, context);
2790}
2791
2792fn bounded_subschemas<'a>(
2793 schema: &'a serde_json::Map<String, Value>,
2794 keyword: &str,
2795 path: &str,
2796 errors: &mut Vec<ValidationError>,
2797) -> Option<&'a [Value]> {
2798 let subschemas = schema.get(keyword)?.as_array()?;
2799 if subschemas.len() > MAX_COMPOSITION_BRANCHES {
2800 push_error(
2801 errors,
2802 path,
2803 format!("{keyword} exceeds composition branch limit"),
2804 );
2805 return None;
2806 }
2807 Some(subschemas)
2808}
2809
2810fn validate_all_of(
2811 schema: &serde_json::Map<String, Value>,
2812 value: &Value,
2813 path: &str,
2814 errors: &mut Vec<ValidationError>,
2815 context: &mut ValidationContext<'_>,
2816) {
2817 if let Some(subschemas) = bounded_subschemas(schema, "allOf", path, errors) {
2818 for subschema in subschemas {
2819 validate_internal(subschema, value, path, errors, context);
2820 }
2821 }
2822}
2823
2824fn validate_any_of(
2825 schema: &serde_json::Map<String, Value>,
2826 value: &Value,
2827 path: &str,
2828 errors: &mut Vec<ValidationError>,
2829 context: &mut ValidationContext<'_>,
2830) {
2831 let Some(subschemas) = bounded_subschemas(schema, "anyOf", path, errors) else {
2832 return;
2833 };
2834 let mut matched = false;
2835 for subschema in subschemas {
2836 if branch_is_valid(subschema, value, path, context) {
2837 matched = true;
2838 break;
2839 }
2840 }
2841 if !matched {
2842 push_error(errors, path, "no subschema in anyOf matched");
2843 }
2844}
2845
2846fn validate_one_of(
2847 schema: &serde_json::Map<String, Value>,
2848 value: &Value,
2849 path: &str,
2850 errors: &mut Vec<ValidationError>,
2851 context: &mut ValidationContext<'_>,
2852) {
2853 let Some(subschemas) = bounded_subschemas(schema, "oneOf", path, errors) else {
2854 return;
2855 };
2856 let mut matches = 0;
2857 for subschema in subschemas {
2858 if branch_is_valid(subschema, value, path, context) {
2859 matches += 1;
2860 }
2861 }
2862 if matches != 1 {
2863 push_error(errors, path, "exactly one subschema in oneOf must match");
2864 }
2865}
2866
2867fn validate_not(
2868 schema: &serde_json::Map<String, Value>,
2869 value: &Value,
2870 path: &str,
2871 errors: &mut Vec<ValidationError>,
2872 context: &mut ValidationContext<'_>,
2873) {
2874 if let Some(subschema) = schema.get("not") {
2875 if branch_is_valid(subschema, value, path, context) {
2876 push_error(errors, path, "value must not match the not subschema");
2877 }
2878 }
2879}
2880
2881fn validate_conditional(
2882 schema: &serde_json::Map<String, Value>,
2883 value: &Value,
2884 path: &str,
2885 errors: &mut Vec<ValidationError>,
2886 context: &mut ValidationContext<'_>,
2887) {
2888 let Some(condition) = schema.get("if") else {
2889 return;
2890 };
2891 let branch_keyword = if branch_is_valid(condition, value, path, context) {
2892 "then"
2893 } else {
2894 "else"
2895 };
2896 if let Some(subschema) = schema.get(branch_keyword) {
2897 validate_internal(subschema, value, path, errors, context);
2898 }
2899}
2900
2901fn branch_is_valid(
2902 schema: &Value,
2903 value: &Value,
2904 path: &str,
2905 context: &mut ValidationContext<'_>,
2906) -> bool {
2907 if context.enforce_unevaluated_properties && !context.consume_work() {
2908 return false;
2909 }
2910 let mut branch_errors = Vec::new();
2911 validate_internal(schema, value, path, &mut branch_errors, context);
2912 branch_errors.is_empty()
2913}
2914
2915fn validate_type(type_val: &Value, value: &Value, final_semantics: bool) -> bool {
2917 match type_val {
2918 Value::String(t) => matches_type(t, value, final_semantics),
2919 Value::Array(types) => types.iter().any(|t| {
2920 t.as_str()
2921 .is_some_and(|type_str| matches_type(type_str, value, final_semantics))
2922 }),
2923 _ => true, }
2925}
2926
2927fn matches_type(type_name: &str, value: &Value, final_semantics: bool) -> bool {
2929 match type_name {
2930 "string" => value.is_string(),
2931 "number" => value.is_number(),
2932 "integer" if final_semantics => value
2933 .as_number()
2934 .and_then(ExactDecimal::from_number)
2935 .is_some_and(|number| number.is_integer()),
2936 "integer" => value.is_i64() || value.is_u64(),
2937 "boolean" => value.is_boolean(),
2938 "object" => value.is_object(),
2939 "array" => value.is_array(),
2940 "null" => value.is_null(),
2941 _ => true, }
2943}
2944
2945fn json_type_name(value: &Value) -> &'static str {
2947 match value {
2948 Value::Null => "null",
2949 Value::Bool(_) => "boolean",
2950 Value::Number(n) if n.is_i64() || n.is_u64() => "integer",
2951 Value::Number(_) => "number",
2952 Value::String(_) => "string",
2953 Value::Array(_) => "array",
2954 Value::Object(_) => "object",
2955 }
2956}
2957
2958fn json_type_name_with_final_semantics(value: &Value, final_semantics: bool) -> &'static str {
2959 if !final_semantics {
2960 return json_type_name(value);
2961 }
2962 match value {
2963 Value::Number(number)
2964 if ExactDecimal::from_number(number).is_some_and(|number| number.is_integer()) =>
2965 {
2966 "integer"
2967 }
2968 _ => json_type_name(value),
2969 }
2970}
2971
2972fn validate_object(
2974 schema: &serde_json::Map<String, Value>,
2975 value: &Value,
2976 obj: &serde_json::Map<String, Value>,
2977 path: &str,
2978 errors: &mut Vec<ValidationError>,
2979 context: &mut ValidationContext<'_>,
2980) {
2981 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
2983 for req in required {
2984 if !consume_validation_work(context, path, errors) {
2985 return;
2986 }
2987 if let Some(req_name) = req.as_str() {
2988 if !obj.contains_key(req_name) {
2989 push_error(errors, path, format!("missing required field: {req_name}"));
2990 }
2991 }
2992 }
2993 }
2994
2995 let properties = schema.get("properties").and_then(Value::as_object);
2996 let patterns = compile_pattern_properties(schema, path, errors, context);
2997 if context.work_exhausted {
2998 return;
2999 }
3000
3001 for (key, value) in obj {
3002 let property_path = format!("{path}.{key}");
3003 if let Some(property_schema) = properties.and_then(|properties| properties.get(key)) {
3004 validate_internal(property_schema, value, &property_path, errors, context);
3005 }
3006 for (pattern, pattern_schema) in &patterns {
3007 let Some(matches) =
3008 charged_regex_is_match(pattern, key, &property_path, errors, context)
3009 else {
3010 return;
3011 };
3012 if matches {
3013 validate_internal(pattern_schema, value, &property_path, errors, context);
3014 }
3015 }
3016 }
3017
3018 if let Some(property_name_schema) = schema.get("propertyNames") {
3019 for key in obj.keys() {
3020 let property_path = format!("{path}.{key}");
3021 validate_internal(
3022 property_name_schema,
3023 &Value::String(key.clone()),
3024 &property_path,
3025 errors,
3026 context,
3027 );
3028 }
3029 }
3030
3031 validate_dependencies(schema, value, obj, path, errors, context);
3032
3033 if let Some(additional) = schema.get("additionalProperties") {
3035 for (key, value) in obj {
3036 let mut matches_pattern = false;
3037 for (pattern, _) in &patterns {
3038 let Some(matches) = charged_regex_is_match(pattern, key, path, errors, context)
3039 else {
3040 return;
3041 };
3042 if matches {
3043 matches_pattern = true;
3044 break;
3045 }
3046 }
3047 let is_defined_property = properties
3048 .is_some_and(|properties| properties.contains_key(key))
3049 || matches_pattern;
3050 if !is_defined_property {
3051 match additional {
3052 Value::Bool(false) => {
3053 push_error(
3054 errors,
3055 path,
3056 format!("additional property not allowed: {key}"),
3057 );
3058 }
3059 Value::Object(_) => {
3060 let prop_path = format!("{path}.{key}");
3061 validate_internal(additional, value, &prop_path, errors, context);
3062 }
3063 _ => {}
3064 }
3065 }
3066 }
3067 }
3068
3069 if context.enforce_unevaluated_properties {
3070 validate_unevaluated_properties(schema, value, obj, path, errors, context);
3071 }
3072
3073 if let Some(min) = schema.get("minProperties")
3076 && count_compare_to_schema_bound(obj.len(), min, context.enforce_unevaluated_properties)
3077 == Some(Ordering::Less)
3078 {
3079 push_error(
3080 errors,
3081 path,
3082 format!("object must have at least {min} properties"),
3083 );
3084 }
3085 if let Some(max) = schema.get("maxProperties")
3086 && count_compare_to_schema_bound(obj.len(), max, context.enforce_unevaluated_properties)
3087 == Some(Ordering::Greater)
3088 {
3089 push_error(
3090 errors,
3091 path,
3092 format!("object must have at most {max} properties"),
3093 );
3094 }
3095}
3096
3097fn validate_unevaluated_properties(
3100 schema: &serde_json::Map<String, Value>,
3101 value: &Value,
3102 obj: &serde_json::Map<String, Value>,
3103 path: &str,
3104 errors: &mut Vec<ValidationError>,
3105 context: &mut ValidationContext<'_>,
3106) {
3107 let Some(unevaluated_schema) = schema.get("unevaluatedProperties") else {
3108 return;
3109 };
3110
3111 let mut evaluated = std::collections::HashSet::with_capacity(obj.len());
3112 mark_evaluated_object_properties(
3113 schema,
3114 value,
3115 obj,
3116 path,
3117 false,
3118 &mut evaluated,
3119 errors,
3120 context,
3121 );
3122
3123 for (key, member) in obj {
3124 if !consume_validation_work(context, path, errors) {
3125 return;
3126 }
3127 if !evaluated.contains(key) {
3128 validate_internal(
3129 unevaluated_schema,
3130 member,
3131 &format!("{path}.{key}"),
3132 errors,
3133 context,
3134 );
3135 }
3136 }
3137}
3138
3139fn mark_evaluated_object_properties(
3145 schema: &serde_json::Map<String, Value>,
3146 value: &Value,
3147 obj: &serde_json::Map<String, Value>,
3148 path: &str,
3149 include_unevaluated_properties: bool,
3150 evaluated: &mut std::collections::HashSet<String>,
3151 errors: &mut Vec<ValidationError>,
3152 context: &mut ValidationContext<'_>,
3153) {
3154 if !context.consume_work() {
3155 push_error(errors, path, "schema validation work limit exceeded");
3156 return;
3157 }
3158
3159 let properties = schema.get("properties").and_then(Value::as_object);
3160 let patterns = compile_pattern_properties(schema, path, errors, context);
3161 if context.work_exhausted {
3162 return;
3163 }
3164 for key in obj.keys() {
3165 if !consume_validation_work(context, path, errors) {
3166 return;
3167 }
3168 let matched_property = properties.is_some_and(|properties| properties.contains_key(key));
3169 let mut matched_pattern = false;
3170 for (pattern, _) in &patterns {
3171 let Some(matches) = charged_regex_is_match(pattern, key, path, errors, context) else {
3172 return;
3173 };
3174 if matches {
3175 matched_pattern = true;
3176 break;
3177 }
3178 }
3179 if matched_property || matched_pattern || schema.contains_key("additionalProperties") {
3180 evaluated.insert(key.clone());
3181 }
3182 }
3183
3184 if include_unevaluated_properties && schema.contains_key("unevaluatedProperties") {
3185 for key in obj.keys() {
3186 if !consume_validation_work(context, path, errors) {
3187 return;
3188 }
3189 evaluated.insert(key.clone());
3190 }
3191 }
3192
3193 if let Some(reference) = schema.get("$ref").and_then(Value::as_str) {
3194 if !context.enter_reference() {
3195 push_error(errors, path, "local schema reference depth limit exceeded");
3196 } else {
3197 let root_schema = context.root_schema;
3198 match resolve_local_reference_with_work(root_schema, schema, reference, context) {
3199 Ok(target) if branch_is_valid(target, value, path, context) => {
3200 if let Some(target) = target.as_object() {
3201 mark_evaluated_object_properties(
3202 target, value, obj, path, true, evaluated, errors, context,
3203 );
3204 }
3205 }
3206 Ok(_) => {}
3207 Err(message) => push_error(errors, path, message),
3208 }
3209 context.leave_reference();
3210 }
3211 }
3212
3213 if let Some(reference) = schema.get("$dynamicRef").and_then(Value::as_str) {
3214 if !context.enter_reference() {
3215 push_error(errors, path, "local schema reference depth limit exceeded");
3216 } else {
3217 let root_schema = context.root_schema;
3218 let target =
3219 resolve_local_reference_with_work(root_schema, schema, reference, context).ok();
3220 if let Some(target) = target {
3221 let dynamic_anchor = target
3222 .as_object()
3223 .and_then(|object| object.get("$dynamicAnchor"))
3224 .and_then(Value::as_str);
3225 let selected = dynamic_anchor
3226 .and_then(|name| context.dynamic_anchor_target(name))
3227 .unwrap_or_else(|| target.clone());
3228 if branch_is_valid(&selected, value, path, context)
3229 && let Some(selected) = selected.as_object()
3230 {
3231 mark_evaluated_object_properties(
3232 selected, value, obj, path, true, evaluated, errors, context,
3233 );
3234 }
3235 }
3236 context.leave_reference();
3237 }
3238 }
3239
3240 if let Some(dependent_schemas) = schema.get("dependentSchemas").and_then(Value::as_object) {
3241 for (trigger, dependent_schema) in dependent_schemas {
3242 if obj.contains_key(trigger)
3243 && branch_is_valid(dependent_schema, value, path, context)
3244 && let Some(dependent_schema) = dependent_schema.as_object()
3245 {
3246 mark_evaluated_object_properties(
3247 dependent_schema,
3248 value,
3249 obj,
3250 path,
3251 true,
3252 evaluated,
3253 errors,
3254 context,
3255 );
3256 }
3257 }
3258 }
3259
3260 mark_composition_evaluated_properties(
3261 schema, "allOf", value, obj, path, evaluated, errors, context,
3262 );
3263 mark_composition_evaluated_properties(
3264 schema, "anyOf", value, obj, path, evaluated, errors, context,
3265 );
3266
3267 if let Some(subschemas) = bounded_subschemas(schema, "oneOf", path, errors) {
3268 let matching: Vec<_> = subschemas
3269 .iter()
3270 .filter(|subschema| branch_is_valid(subschema, value, path, context))
3271 .collect();
3272 if matching.len() == 1
3273 && let Some(subschema) = matching[0].as_object()
3274 {
3275 mark_evaluated_object_properties(
3276 subschema, value, obj, path, true, evaluated, errors, context,
3277 );
3278 }
3279 }
3280
3281 if let Some(condition) = schema.get("if") {
3282 let condition_matched = branch_is_valid(condition, value, path, context);
3283 if condition_matched && let Some(condition) = condition.as_object() {
3284 mark_evaluated_object_properties(
3285 condition, value, obj, path, true, evaluated, errors, context,
3286 );
3287 }
3288
3289 let branch = if condition_matched { "then" } else { "else" };
3290 if let Some(subschema) = schema.get(branch)
3291 && branch_is_valid(subschema, value, path, context)
3292 && let Some(subschema) = subschema.as_object()
3293 {
3294 mark_evaluated_object_properties(
3295 subschema, value, obj, path, true, evaluated, errors, context,
3296 );
3297 }
3298 }
3299}
3300
3301fn mark_composition_evaluated_properties(
3303 schema: &serde_json::Map<String, Value>,
3304 keyword: &str,
3305 value: &Value,
3306 obj: &serde_json::Map<String, Value>,
3307 path: &str,
3308 evaluated: &mut std::collections::HashSet<String>,
3309 errors: &mut Vec<ValidationError>,
3310 context: &mut ValidationContext<'_>,
3311) {
3312 let Some(subschemas) = bounded_subschemas(schema, keyword, path, errors) else {
3313 return;
3314 };
3315 for subschema in subschemas {
3316 if branch_is_valid(subschema, value, path, context)
3317 && let Some(subschema) = subschema.as_object()
3318 {
3319 mark_evaluated_object_properties(
3320 subschema, value, obj, path, true, evaluated, errors, context,
3321 );
3322 }
3323 }
3324}
3325
3326fn compile_pattern_properties<'a>(
3327 schema: &'a serde_json::Map<String, Value>,
3328 path: &str,
3329 errors: &mut Vec<ValidationError>,
3330 context: &mut ValidationContext<'_>,
3331) -> Vec<(Regex, &'a Value)> {
3332 let Some(pattern_properties) = schema.get("patternProperties").and_then(Value::as_object)
3333 else {
3334 return Vec::new();
3335 };
3336 if pattern_properties.len() > MAX_PATTERN_PROPERTIES {
3337 push_error(errors, path, "patternProperties exceeds entry limit");
3338 return Vec::new();
3339 }
3340
3341 let mut patterns = Vec::with_capacity(pattern_properties.len());
3342 for (source, pattern_schema) in pattern_properties {
3343 if !consume_validation_work(context, path, errors) {
3344 break;
3345 }
3346 if source.len() > MAX_PATTERN_PROPERTY_BYTES {
3347 push_error(errors, path, "patternProperties pattern exceeds byte limit");
3348 continue;
3349 }
3350 match Regex::new(source) {
3351 Ok(pattern) => patterns.push((pattern, pattern_schema)),
3352 Err(_) => push_error(errors, path, "invalid patternProperties pattern"),
3353 }
3354 }
3355 patterns
3356}
3357
3358fn validate_dependencies(
3359 schema: &serde_json::Map<String, Value>,
3360 value: &Value,
3361 obj: &serde_json::Map<String, Value>,
3362 path: &str,
3363 errors: &mut Vec<ValidationError>,
3364 context: &mut ValidationContext<'_>,
3365) {
3366 if let Some(dependent_required) = schema.get("dependentRequired").and_then(Value::as_object) {
3367 for (trigger, required) in dependent_required {
3368 if !consume_validation_work(context, path, errors) {
3369 return;
3370 }
3371 if !obj.contains_key(trigger) {
3372 continue;
3373 }
3374 if let Some(required) = required.as_array() {
3375 for required_property in required {
3376 if !consume_validation_work(context, path, errors) {
3377 return;
3378 }
3379 if let Some(required_property) = required_property.as_str() {
3380 if !obj.contains_key(required_property) {
3381 push_error(
3382 errors,
3383 path,
3384 format!("property {trigger} requires property {required_property}"),
3385 );
3386 }
3387 }
3388 }
3389 }
3390 }
3391 }
3392
3393 if let Some(dependent_schemas) = schema.get("dependentSchemas").and_then(Value::as_object) {
3394 for (trigger, dependent_schema) in dependent_schemas {
3395 if obj.contains_key(trigger) {
3396 validate_internal(dependent_schema, value, path, errors, context);
3397 }
3398 }
3399 }
3400}
3401
3402fn validate_array(
3404 schema: &serde_json::Map<String, Value>,
3405 arr: &[Value],
3406 path: &str,
3407 errors: &mut Vec<ValidationError>,
3408 context: &mut ValidationContext<'_>,
3409) {
3410 let mut prefix_len = 0;
3412 if let Some(prefix_items) = schema.get("prefixItems").and_then(|v| v.as_array()) {
3413 prefix_len = prefix_items.len();
3414 for (i, item_schema) in prefix_items.iter().enumerate() {
3415 if let Some(item) = arr.get(i) {
3416 let item_path = format!("{path}[{i}]");
3417 validate_internal(item_schema, item, &item_path, errors, context);
3418 }
3419 }
3420 }
3421
3422 if let Some(items_schema) = schema.get("items") {
3424 if items_schema.is_array() && prefix_len == 0 {
3426 if let Some(items_arr) = items_schema.as_array() {
3427 for (i, item_schema) in items_arr.iter().enumerate() {
3428 if let Some(item) = arr.get(i) {
3429 let item_path = format!("{path}[{i}]");
3430 validate_internal(item_schema, item, &item_path, errors, context);
3431 }
3432 }
3433 }
3435 } else if items_schema.is_object() || items_schema.is_boolean() {
3436 for (i, item) in arr.iter().enumerate().skip(prefix_len) {
3438 let item_path = format!("{path}[{i}]");
3439 validate_internal(items_schema, item, &item_path, errors, context);
3440 }
3441 }
3442 }
3443
3444 if let Some(min) = schema.get("minItems")
3447 && count_compare_to_schema_bound(arr.len(), min, context.enforce_unevaluated_properties)
3448 == Some(Ordering::Less)
3449 {
3450 push_error(
3451 errors,
3452 path,
3453 format!("array must have at least {min} items"),
3454 );
3455 }
3456 if let Some(max) = schema.get("maxItems")
3457 && count_compare_to_schema_bound(arr.len(), max, context.enforce_unevaluated_properties)
3458 == Some(Ordering::Greater)
3459 {
3460 push_error(errors, path, format!("array must have at most {max} items"));
3461 }
3462
3463 if schema
3465 .get("uniqueItems")
3466 .and_then(serde_json::Value::as_bool)
3467 .unwrap_or(false)
3468 {
3469 if context.enforce_unevaluated_properties {
3470 for (index, item) in arr.iter().enumerate() {
3475 for previous in &arr[..index] {
3476 if !consume_validation_work(context, path, errors) {
3477 return;
3478 }
3479 let Some(equal) =
3480 json_schema_equal_with_work(previous, item, path, errors, context)
3481 else {
3482 return;
3483 };
3484 if equal {
3485 push_error(
3486 errors,
3487 &format!("{path}[{index}]"),
3488 "duplicate item in array",
3489 );
3490 break;
3491 }
3492 }
3493 }
3494 } else {
3495 let mut seen = std::collections::HashSet::with_capacity(arr.len());
3497 for (index, item) in arr.iter().enumerate() {
3498 let key = serde_json::to_string(item).unwrap_or_default();
3499 if !seen.insert(key) {
3500 push_error(
3501 errors,
3502 &format!("{path}[{index}]"),
3503 "duplicate item in array",
3504 );
3505 }
3506 }
3507 }
3508 }
3509
3510 if let Some(contains) = schema.get("contains") {
3511 let mut matches = 0;
3512 for item in arr {
3513 if branch_is_valid(contains, item, path, context) {
3514 matches += 1;
3515 }
3516 }
3517 let below_minimum = if context.enforce_unevaluated_properties {
3518 schema.get("minContains").map_or(matches == 0, |minimum| {
3519 count_compare_to_schema_bound(matches, minimum, true) == Some(Ordering::Less)
3520 })
3521 } else {
3522 let minimum = schema
3523 .get("minContains")
3524 .and_then(Value::as_u64)
3525 .unwrap_or(1);
3526 (matches as u64) < minimum
3527 };
3528 if below_minimum {
3529 let minimum = schema
3530 .get("minContains")
3531 .map_or_else(|| "1".to_owned(), |minimum| minimum.to_string());
3532 push_error(
3533 errors,
3534 path,
3535 format!("array must contain at least {minimum} matching items"),
3536 );
3537 }
3538 if let Some(maximum) = schema.get("maxContains")
3539 && count_compare_to_schema_bound(
3540 matches,
3541 maximum,
3542 context.enforce_unevaluated_properties,
3543 ) == Some(Ordering::Greater)
3544 {
3545 push_error(
3546 errors,
3547 path,
3548 format!("array must contain at most {maximum} matching items"),
3549 );
3550 }
3551 }
3552
3553 if context.enforce_unevaluated_properties {
3554 validate_unevaluated_items(schema, arr, path, errors, context);
3555 }
3556}
3557
3558fn validate_unevaluated_items(
3561 schema: &serde_json::Map<String, Value>,
3562 arr: &[Value],
3563 path: &str,
3564 errors: &mut Vec<ValidationError>,
3565 context: &mut ValidationContext<'_>,
3566) {
3567 let Some(unevaluated_schema) = schema.get("unevaluatedItems") else {
3568 return;
3569 };
3570 let mut evaluated = HashSet::with_capacity(arr.len());
3571 mark_evaluated_array_items(schema, arr, path, false, &mut evaluated, errors, context);
3572
3573 for (index, item) in arr.iter().enumerate() {
3574 if !consume_validation_work(context, path, errors) {
3575 return;
3576 }
3577 if !evaluated.contains(&index) {
3578 validate_internal(
3579 unevaluated_schema,
3580 item,
3581 &format!("{path}[{index}]"),
3582 errors,
3583 context,
3584 );
3585 }
3586 }
3587}
3588
3589fn mark_evaluated_array_items(
3590 schema: &serde_json::Map<String, Value>,
3591 arr: &[Value],
3592 path: &str,
3593 include_unevaluated_items: bool,
3594 evaluated: &mut HashSet<usize>,
3595 errors: &mut Vec<ValidationError>,
3596 context: &mut ValidationContext<'_>,
3597) {
3598 if !consume_validation_work(context, path, errors) {
3599 return;
3600 }
3601
3602 if let Some(prefix_items) = schema.get("prefixItems").and_then(Value::as_array) {
3603 for (index, item_schema) in prefix_items.iter().enumerate() {
3604 let Some(item) = arr.get(index) else {
3605 break;
3606 };
3607 if !consume_validation_work(context, path, errors) {
3608 return;
3609 }
3610 if branch_is_valid(item_schema, item, &format!("{path}[{index}]"), context) {
3611 evaluated.insert(index);
3612 }
3613 }
3614 }
3615
3616 let prefix_len = schema
3617 .get("prefixItems")
3618 .and_then(Value::as_array)
3619 .map_or(0, Vec::len);
3620 if let Some(items_schema) = schema.get("items") {
3621 for (index, item) in arr.iter().enumerate().skip(prefix_len) {
3622 if !consume_validation_work(context, path, errors) {
3623 return;
3624 }
3625 if branch_is_valid(items_schema, item, &format!("{path}[{index}]"), context) {
3626 evaluated.insert(index);
3627 }
3628 }
3629 }
3630
3631 if let Some(contains_schema) = schema.get("contains") {
3632 let matches: Vec<usize> = arr
3633 .iter()
3634 .enumerate()
3635 .filter_map(|(index, item)| {
3636 consume_validation_work(context, path, errors)
3637 .then(|| {
3638 branch_is_valid(contains_schema, item, &format!("{path}[{index}]"), context)
3639 })
3640 .filter(|matches| *matches)
3641 .map(|_| index)
3642 })
3643 .collect();
3644 let meets_minimum = schema
3645 .get("minContains")
3646 .map_or(matches.len() >= 1, |minimum| {
3647 count_compare_to_schema_bound(matches.len(), minimum, true) != Some(Ordering::Less)
3648 });
3649 let within_maximum = schema.get("maxContains").is_none_or(|maximum| {
3650 count_compare_to_schema_bound(matches.len(), maximum, true) != Some(Ordering::Greater)
3651 });
3652 if meets_minimum && within_maximum {
3653 evaluated.extend(matches);
3654 }
3655 }
3656
3657 if include_unevaluated_items && schema.contains_key("unevaluatedItems") {
3658 for index in 0..arr.len() {
3659 if !consume_validation_work(context, path, errors) {
3660 return;
3661 }
3662 evaluated.insert(index);
3663 }
3664 }
3665
3666 if let Some(reference) = schema.get("$ref").and_then(Value::as_str) {
3667 mark_reference_evaluated_array_items(
3668 schema, reference, arr, path, evaluated, errors, context,
3669 );
3670 }
3671 if let Some(reference) = schema.get("$dynamicRef").and_then(Value::as_str) {
3672 mark_dynamic_reference_evaluated_array_items(
3673 schema, reference, arr, path, evaluated, errors, context,
3674 );
3675 }
3676
3677 mark_composition_evaluated_array_items(schema, "allOf", arr, path, evaluated, errors, context);
3678 mark_composition_evaluated_array_items(schema, "anyOf", arr, path, evaluated, errors, context);
3679
3680 if let Some(subschemas) = bounded_subschemas(schema, "oneOf", path, errors) {
3681 let matching: Vec<_> = subschemas
3682 .iter()
3683 .filter(|subschema| {
3684 branch_is_valid(subschema, &Value::Array(arr.to_vec()), path, context)
3685 })
3686 .collect();
3687 if matching.len() == 1
3688 && let Some(subschema) = matching[0].as_object()
3689 {
3690 mark_evaluated_array_items(subschema, arr, path, true, evaluated, errors, context);
3691 }
3692 }
3693
3694 if let Some(condition) = schema.get("if") {
3695 let value = Value::Array(arr.to_vec());
3696 let condition_matched = branch_is_valid(condition, &value, path, context);
3697 if condition_matched && let Some(condition) = condition.as_object() {
3698 mark_evaluated_array_items(condition, arr, path, true, evaluated, errors, context);
3699 }
3700 let branch = if condition_matched { "then" } else { "else" };
3701 if let Some(subschema) = schema.get(branch)
3702 && branch_is_valid(subschema, &value, path, context)
3703 && let Some(subschema) = subschema.as_object()
3704 {
3705 mark_evaluated_array_items(subschema, arr, path, true, evaluated, errors, context);
3706 }
3707 }
3708}
3709
3710fn mark_reference_evaluated_array_items(
3711 schema: &serde_json::Map<String, Value>,
3712 reference: &str,
3713 arr: &[Value],
3714 path: &str,
3715 evaluated: &mut HashSet<usize>,
3716 errors: &mut Vec<ValidationError>,
3717 context: &mut ValidationContext<'_>,
3718) {
3719 if !context.enter_reference() {
3720 push_error(errors, path, "local schema reference depth limit exceeded");
3721 return;
3722 }
3723 let root_schema = context.root_schema;
3724 let target = resolve_local_reference_with_work(root_schema, schema, reference, context).ok();
3725 if let Some(target) = target
3726 && branch_is_valid(target, &Value::Array(arr.to_vec()), path, context)
3727 && let Some(target) = target.as_object()
3728 {
3729 mark_evaluated_array_items(target, arr, path, true, evaluated, errors, context);
3730 }
3731 context.leave_reference();
3732}
3733
3734fn mark_dynamic_reference_evaluated_array_items(
3735 schema: &serde_json::Map<String, Value>,
3736 reference: &str,
3737 arr: &[Value],
3738 path: &str,
3739 evaluated: &mut HashSet<usize>,
3740 errors: &mut Vec<ValidationError>,
3741 context: &mut ValidationContext<'_>,
3742) {
3743 if !context.enter_reference() {
3744 push_error(errors, path, "local schema reference depth limit exceeded");
3745 return;
3746 }
3747 let root_schema = context.root_schema;
3748 let target = resolve_local_reference_with_work(root_schema, schema, reference, context).ok();
3749 if let Some(target) = target {
3750 let dynamic_anchor = target
3751 .as_object()
3752 .and_then(|object| object.get("$dynamicAnchor"))
3753 .and_then(Value::as_str);
3754 let selected = dynamic_anchor
3755 .and_then(|name| context.dynamic_anchor_target(name))
3756 .unwrap_or_else(|| target.clone());
3757 if branch_is_valid(&selected, &Value::Array(arr.to_vec()), path, context)
3758 && let Some(selected) = selected.as_object()
3759 {
3760 mark_evaluated_array_items(selected, arr, path, true, evaluated, errors, context);
3761 }
3762 }
3763 context.leave_reference();
3764}
3765
3766fn mark_composition_evaluated_array_items(
3767 schema: &serde_json::Map<String, Value>,
3768 keyword: &str,
3769 arr: &[Value],
3770 path: &str,
3771 evaluated: &mut HashSet<usize>,
3772 errors: &mut Vec<ValidationError>,
3773 context: &mut ValidationContext<'_>,
3774) {
3775 let Some(subschemas) = bounded_subschemas(schema, keyword, path, errors) else {
3776 return;
3777 };
3778 let value = Value::Array(arr.to_vec());
3779 for subschema in subschemas {
3780 if branch_is_valid(subschema, &value, path, context)
3781 && let Some(subschema) = subschema.as_object()
3782 {
3783 mark_evaluated_array_items(subschema, arr, path, true, evaluated, errors, context);
3784 }
3785 }
3786}
3787
3788fn validate_string(
3790 schema: &serde_json::Map<String, Value>,
3791 s: &str,
3792 path: &str,
3793 errors: &mut Vec<ValidationError>,
3794 context: &mut ValidationContext<'_>,
3795) {
3796 let len = s.chars().count();
3799 if let Some(min) = schema.get("minLength")
3800 && count_compare_to_schema_bound(len, min, context.enforce_unevaluated_properties)
3801 == Some(Ordering::Less)
3802 {
3803 push_error(
3804 errors,
3805 path,
3806 format!("string must be at least {min} characters"),
3807 );
3808 }
3809 if let Some(max) = schema.get("maxLength")
3810 && count_compare_to_schema_bound(len, max, context.enforce_unevaluated_properties)
3811 == Some(Ordering::Greater)
3812 {
3813 push_error(
3814 errors,
3815 path,
3816 format!("string must be at most {max} characters"),
3817 );
3818 }
3819
3820 if let Some(pattern) = schema.get("pattern").and_then(serde_json::Value::as_str) {
3822 if !consume_validation_work(context, path, errors) {
3823 return;
3824 }
3825 match Regex::new(pattern) {
3826 Ok(re) => {
3827 let Some(matches) = charged_regex_is_match(&re, s, path, errors, context) else {
3828 return;
3829 };
3830 if !matches {
3831 push_error(
3832 errors,
3833 path,
3834 format!("string does not match pattern {pattern:?}"),
3835 );
3836 }
3837 }
3838 Err(e) => {
3839 push_error(
3841 errors,
3842 path,
3843 format!("invalid schema pattern {pattern:?}: {e}"),
3844 );
3845 }
3846 }
3847 }
3848}
3849
3850fn is_valid_uri_scheme(scheme: &str) -> bool {
3851 let bytes = scheme.as_bytes();
3852 bytes.first().is_some_and(u8::is_ascii_alphabetic)
3853 && bytes
3854 .iter()
3855 .skip(1)
3856 .all(|byte| byte.is_ascii_alphanumeric() || matches!(*byte, b'+' | b'-' | b'.'))
3857}
3858
3859#[derive(Debug, Clone, PartialEq, Eq)]
3865struct ExactDecimal {
3866 negative: bool,
3867 digits: String,
3868 exponent: i64,
3869}
3870
3871impl ExactDecimal {
3872 fn from_value(value: &Value) -> Option<Self> {
3873 value.as_number().and_then(Self::from_number)
3874 }
3875
3876 fn from_number(number: &serde_json::Number) -> Option<Self> {
3877 Self::parse(&number.to_string())
3878 }
3879
3880 fn parse(source: &str) -> Option<Self> {
3881 if source.is_empty() || source.len() > MAX_EXACT_DECIMAL_DIGITS {
3882 return None;
3883 }
3884 let (negative, unsigned) = match source.as_bytes().first() {
3885 Some(b'-') => (true, &source[1..]),
3886 _ => (false, source),
3887 };
3888 let (significand, exponent) = match unsigned.split_once(['e', 'E']) {
3889 Some((significand, exponent)) => {
3890 let exponent = exponent.parse::<i64>().ok()?;
3891 if exponent.unsigned_abs() as usize > MAX_EXACT_DECIMAL_DIGITS {
3892 return None;
3893 }
3894 (significand, exponent)
3895 }
3896 None => (unsigned, 0),
3897 };
3898 let (whole, fraction) = match significand.split_once('.') {
3899 Some((whole, fraction)) => (whole, fraction),
3900 None => (significand, ""),
3901 };
3902 if whole.is_empty()
3903 || !whole.bytes().all(|byte| byte.is_ascii_digit())
3904 || !fraction.bytes().all(|byte| byte.is_ascii_digit())
3905 {
3906 return None;
3907 }
3908 let coefficient = format!("{whole}{fraction}");
3909 let significant = coefficient.trim_start_matches('0');
3910 if significant.is_empty() {
3911 return Some(Self {
3912 negative: false,
3913 digits: "0".to_owned(),
3914 exponent: 0,
3915 });
3916 }
3917 if significant.len() > MAX_EXACT_DECIMAL_DIGITS {
3918 return None;
3919 }
3920 let exponent = exponent.checked_sub(i64::try_from(fraction.len()).ok()?)?;
3921 if exponent.unsigned_abs() as usize > MAX_EXACT_DECIMAL_DIGITS {
3922 return None;
3923 }
3924 Some(Self {
3925 negative,
3926 digits: significant.to_owned(),
3927 exponent,
3928 })
3929 }
3930
3931 fn is_zero(&self) -> bool {
3932 self.digits.as_bytes()[0] == b'0'
3933 }
3934
3935 fn is_positive(&self) -> bool {
3936 !self.negative && !self.is_zero()
3937 }
3938
3939 fn is_integer(&self) -> bool {
3940 self.exponent >= 0
3941 || self
3942 .digits
3943 .bytes()
3944 .rev()
3945 .take(self.exponent.unsigned_abs() as usize)
3946 .all(|digit| digit == b'0')
3947 }
3948
3949 fn compare(&self, other: &Self) -> Ordering {
3950 match (self.negative, other.negative) {
3951 (true, false) => return Ordering::Less,
3952 (false, true) => return Ordering::Greater,
3953 _ => {}
3954 }
3955 let magnitude = self.compare_magnitude(other);
3956 if self.negative {
3957 magnitude.reverse()
3958 } else {
3959 magnitude
3960 }
3961 }
3962
3963 fn compare_magnitude(&self, other: &Self) -> Ordering {
3964 match (self.is_zero(), other.is_zero()) {
3965 (true, true) => return Ordering::Equal,
3966 (true, false) => return Ordering::Less,
3967 (false, true) => return Ordering::Greater,
3968 (false, false) => {}
3969 }
3970 let self_position = self.exponent + self.digits.len() as i64;
3971 let other_position = other.exponent + other.digits.len() as i64;
3972 match self_position.cmp(&other_position) {
3973 Ordering::Equal => compare_digit_strings(&self.digits, &other.digits),
3974 order => order,
3975 }
3976 }
3977
3978 fn is_multiple_of_bounded(
3979 &self,
3980 divisor: &Self,
3981 path: &str,
3982 errors: &mut Vec<ValidationError>,
3983 context: &mut ValidationContext<'_>,
3984 ) -> Option<bool> {
3985 if divisor.is_zero() {
3986 return Some(false);
3987 }
3988 if self.is_zero() {
3989 return Some(true);
3990 }
3991 let scale = self.exponent.min(divisor.exponent).min(0).unsigned_abs() as usize;
3992 let dividend_zeros = usize::try_from(self.exponent + scale as i64).ok();
3993 let divisor_zeros = usize::try_from(divisor.exponent + scale as i64).ok();
3994 let (Some(dividend_zeros), Some(divisor_zeros)) = (dividend_zeros, divisor_zeros) else {
3995 return Some(false);
3996 };
3997 if !consume_validation_work_units(
3998 context,
3999 dividend_zeros.saturating_add(divisor_zeros),
4000 path,
4001 errors,
4002 ) {
4003 return None;
4004 }
4005 let mut dividend = self.digits.clone();
4006 dividend.extend(std::iter::repeat_n('0', dividend_zeros));
4007 let mut divisor = divisor.digits.clone();
4008 divisor.extend(std::iter::repeat_n('0', divisor_zeros));
4009 decimal_integer_is_divisible(÷nd, &divisor, path, errors, context)
4010 }
4011}
4012
4013fn count_compare_to_schema_bound(
4019 count: usize,
4020 bound: &Value,
4021 admitted_final: bool,
4022) -> Option<Ordering> {
4023 if !admitted_final {
4024 return bound.as_u64().map(|bound| (count as u64).cmp(&bound));
4025 }
4026 let count = ExactDecimal::parse(&count.to_string())?;
4027 let bound = ExactDecimal::from_value(bound)?;
4028 Some(count.compare(&bound))
4029}
4030
4031fn compare_digit_strings(left: &str, right: &str) -> Ordering {
4032 let shared_length = left.len().max(right.len());
4033 for index in 0..shared_length {
4034 let left_digit = left.as_bytes().get(index).copied().unwrap_or(b'0');
4035 let right_digit = right.as_bytes().get(index).copied().unwrap_or(b'0');
4036 match left_digit.cmp(&right_digit) {
4037 Ordering::Equal => {}
4038 order => return order,
4039 }
4040 }
4041 Ordering::Equal
4042}
4043
4044fn decimal_integer_is_divisible(
4045 dividend: &str,
4046 divisor: &str,
4047 path: &str,
4048 errors: &mut Vec<ValidationError>,
4049 context: &mut ValidationContext<'_>,
4050) -> Option<bool> {
4051 let divisor = divisor.trim_start_matches('0');
4052 if divisor.is_empty() {
4053 return Some(false);
4054 }
4055 let mut remainder = String::new();
4056 for digit in dividend.bytes() {
4057 if !consume_validation_work(context, path, errors) {
4058 return None;
4059 }
4060 if digit != b'0' || !remainder.is_empty() {
4061 remainder.push(char::from(digit));
4062 }
4063 loop {
4064 if !consume_validation_work_units(
4065 context,
4066 remainder.len().min(divisor.len()),
4067 path,
4068 errors,
4069 ) {
4070 return None;
4071 }
4072 if compare_decimal_integers(&remainder, divisor) == Ordering::Less {
4073 break;
4074 }
4075 if !consume_validation_work_units(context, remainder.len(), path, errors) {
4076 return None;
4077 }
4078 subtract_decimal_integers(&mut remainder, divisor);
4079 if !consume_validation_work_units(context, remainder.len(), path, errors) {
4080 return None;
4081 }
4082 trim_decimal_integer(&mut remainder);
4083 }
4084 }
4085 Some(remainder.is_empty() || remainder == "0")
4086}
4087
4088fn consume_validation_work_units(
4089 context: &mut ValidationContext<'_>,
4090 units: usize,
4091 path: &str,
4092 errors: &mut Vec<ValidationError>,
4093) -> bool {
4094 (0..units).all(|_| consume_validation_work(context, path, errors))
4095}
4096
4097fn compare_decimal_integers(left: &str, right: &str) -> Ordering {
4098 match left.len().cmp(&right.len()) {
4099 Ordering::Equal => left.cmp(right),
4100 order => order,
4101 }
4102}
4103
4104fn subtract_decimal_integers(left: &mut String, right: &str) {
4105 let mut digits = left.bytes().collect::<Vec<_>>();
4106 let right = right.as_bytes();
4107 let mut borrow = 0_i16;
4108 for offset in 0..digits.len() {
4109 let left_index = digits.len() - 1 - offset;
4110 let right_digit = right
4111 .get(right.len().saturating_sub(offset + 1))
4112 .map_or(0, |digit| i16::from(*digit - b'0'));
4113 let mut digit = i16::from(digits[left_index] - b'0') - right_digit - borrow;
4114 if digit < 0 {
4115 digit += 10;
4116 borrow = 1;
4117 } else {
4118 borrow = 0;
4119 }
4120 digits[left_index] = digit as u8 + b'0';
4121 }
4122 *left = digits.into_iter().map(char::from).collect();
4126}
4127
4128fn trim_decimal_integer(value: &mut String) {
4129 let first_nonzero = value.bytes().position(|digit| digit != b'0');
4130 match first_nonzero {
4131 Some(index) if index > 0 => {
4132 let _ = value.drain(..index);
4133 }
4134 None => value.clear(),
4135 _ => {}
4136 }
4137}
4138
4139fn json_schema_equal_with_work(
4141 left: &Value,
4142 right: &Value,
4143 path: &str,
4144 errors: &mut Vec<ValidationError>,
4145 context: &mut ValidationContext<'_>,
4146) -> Option<bool> {
4147 if !consume_validation_work(context, path, errors) {
4148 return None;
4149 }
4150 match (left, right) {
4151 (Value::Number(left), Value::Number(right)) => Some(
4152 ExactDecimal::from_number(left)
4153 .zip(ExactDecimal::from_number(right))
4154 .is_some_and(|(left, right)| left.compare(&right) == Ordering::Equal),
4155 ),
4156 (Value::Array(left), Value::Array(right)) => {
4157 if left.len() != right.len() {
4158 return Some(false);
4159 }
4160 for (left, right) in left.iter().zip(right) {
4161 if !json_schema_equal_with_work(left, right, path, errors, context)? {
4162 return Some(false);
4163 }
4164 }
4165 Some(true)
4166 }
4167 (Value::Object(left), Value::Object(right)) => {
4168 if left.len() != right.len() {
4169 return Some(false);
4170 }
4171 for (key, left) in left {
4172 let Some(right) = right.get(key) else {
4173 return Some(false);
4174 };
4175 if !json_schema_equal_with_work(left, right, path, errors, context)? {
4176 return Some(false);
4177 }
4178 }
4179 Some(true)
4180 }
4181 _ => Some(left == right),
4182 }
4183}
4184
4185fn validate_number(
4187 schema: &serde_json::Map<String, Value>,
4188 n: &serde_json::Number,
4189 path: &str,
4190 errors: &mut Vec<ValidationError>,
4191 final_semantics: bool,
4192 context: &mut ValidationContext<'_>,
4193) {
4194 if final_semantics {
4195 validate_exact_number(schema, n, path, errors, context);
4196 return;
4197 }
4198 let val = n.as_f64().unwrap_or(0.0);
4199
4200 if let Some(min) = schema.get("minimum").and_then(serde_json::Value::as_f64) {
4202 if val < min {
4203 push_error(errors, path, format!("value must be >= {min}"));
4204 }
4205 }
4206 if let Some(max) = schema.get("maximum").and_then(serde_json::Value::as_f64) {
4207 if val > max {
4208 push_error(errors, path, format!("value must be <= {max}"));
4209 }
4210 }
4211
4212 if let Some(min) = schema
4214 .get("exclusiveMinimum")
4215 .and_then(serde_json::Value::as_f64)
4216 {
4217 if val <= min {
4218 push_error(errors, path, format!("value must be > {min}"));
4219 }
4220 }
4221 if let Some(max) = schema
4222 .get("exclusiveMaximum")
4223 .and_then(serde_json::Value::as_f64)
4224 {
4225 if val >= max {
4226 push_error(errors, path, format!("value must be < {max}"));
4227 }
4228 }
4229
4230 if let Some(multiple) = schema.get("multipleOf").and_then(serde_json::Value::as_f64) {
4232 if multiple != 0.0 && (val % multiple).abs() > f64::EPSILON {
4233 push_error(
4234 errors,
4235 path,
4236 format!("value must be a multiple of {multiple}"),
4237 );
4238 }
4239 }
4240}
4241
4242fn validate_exact_number(
4243 schema: &serde_json::Map<String, Value>,
4244 number: &serde_json::Number,
4245 path: &str,
4246 errors: &mut Vec<ValidationError>,
4247 context: &mut ValidationContext<'_>,
4248) {
4249 let Some(value) = ExactDecimal::from_number(number) else {
4250 push_error(
4251 errors,
4252 path,
4253 "instance number exceeds exact comparison bound",
4254 );
4255 return;
4256 };
4257 for (keyword, allowed) in [
4258 ("minimum", ">="),
4259 ("maximum", "<="),
4260 ("exclusiveMinimum", ">"),
4261 ("exclusiveMaximum", "<"),
4262 ] {
4263 let Some(bound_value) = schema.get(keyword) else {
4264 continue;
4265 };
4266 let bound_description = bound_value.to_string();
4267 let Some(bound) = ExactDecimal::from_value(bound_value) else {
4268 push_error(
4269 errors,
4270 path,
4271 "schema numeric keyword must be a bounded exact number",
4272 );
4273 continue;
4274 };
4275 let comparison = value.compare(&bound);
4276 let invalid = match keyword {
4277 "minimum" => comparison == Ordering::Less,
4278 "maximum" => comparison == Ordering::Greater,
4279 "exclusiveMinimum" => comparison != Ordering::Greater,
4280 "exclusiveMaximum" => comparison != Ordering::Less,
4281 _ => unreachable!("the numeric keyword set is fixed"),
4282 };
4283 if invalid {
4284 push_error(
4285 errors,
4286 path,
4287 format!("value must be {allowed} {bound_description}"),
4288 );
4289 }
4290 }
4291 if let Some(multiple) = schema.get("multipleOf").and_then(ExactDecimal::from_value) {
4292 if let Some(false) = value.is_multiple_of_bounded(&multiple, path, errors, context) {
4293 push_error(
4294 errors,
4295 path,
4296 "value must be a multiple of the exact schema divisor",
4297 );
4298 }
4299 }
4300}
4301
4302#[cfg(test)]
4303mod tests {
4304 use super::*;
4305 use serde_json::json;
4306
4307 fn sch_01_a_schema() -> Value {
4308 json!({
4309 "$defs": {
4310 "positive-id": {"type": "integer", "minimum": 1}
4311 },
4312 "type": "object",
4313 "properties": {
4314 "id": {"$ref": "#/$defs/positive-id"},
4315 "mode": {"enum": ["fast", "safe"]},
4316 "items": {
4317 "type": "array",
4318 "contains": {"type": "integer", "multipleOf": 2},
4319 "minContains": 2,
4320 "maxContains": 2
4321 }
4322 },
4323 "required": ["id", "mode", "items"],
4324 "patternProperties": {
4325 "^x-": {"type": "string"}
4326 },
4327 "propertyNames": {"pattern": "^[A-Za-z-]+$"},
4328 "dependentRequired": {
4329 "creditCard": ["billingAddress"]
4330 },
4331 "dependentSchemas": {
4332 "creditCard": {"required": ["billingAddress"]}
4333 },
4334 "allOf": [{"required": ["id"]}],
4335 "anyOf": [
4336 {"properties": {"mode": {"const": "fast"}}, "required": ["mode"]},
4337 {"properties": {"mode": {"const": "safe"}}, "required": ["mode"]}
4338 ],
4339 "oneOf": [
4340 {"properties": {"mode": {"const": "fast"}}, "required": ["mode"]},
4341 {"properties": {"mode": {"const": "safe"}}, "required": ["mode"]}
4342 ],
4343 "not": {
4344 "properties": {"mode": {"const": "disabled"}},
4345 "required": ["mode"]
4346 },
4347 "if": {
4348 "properties": {"mode": {"const": "fast"}},
4349 "required": ["mode"]
4350 },
4351 "then": {"required": ["fastConfig"]},
4352 "else": {"required": ["safeConfig"]}
4353 })
4354 }
4355
4356 fn sch_01_a_valid_instance() -> Value {
4357 json!({
4358 "id": 7,
4359 "mode": "fast",
4360 "fastConfig": true,
4361 "items": [2, 4, 5],
4362 "x-label": "bounded",
4363 "creditCard": "4111",
4364 "billingAddress": "42 Schema Street"
4365 })
4366 }
4367
4368 #[test]
4369 fn sch_01_a_positive() {
4370 let schema = sch_01_a_schema();
4371 let instance = sch_01_a_valid_instance();
4372
4373 assert!(validate(&schema, &instance).is_ok());
4374 }
4375
4376 #[test]
4377 fn sch_01_a_planted_negative() {
4378 let schema = sch_01_a_schema();
4379 let schema_before = schema.clone();
4380 let mut instance = sch_01_a_valid_instance();
4381 instance["items"][1] = json!(3);
4382
4383 let errors = validate(&schema, &instance)
4384 .expect_err("changing only one array item must violate minContains");
4385 assert_eq!(errors.len(), 1);
4386 assert_eq!(errors[0].path, "root.items");
4387 assert_eq!(
4388 errors[0].message,
4389 "array must contain at least 2 matching items"
4390 );
4391 assert_eq!(schema, schema_before);
4392 }
4393
4394 #[test]
4395 fn final_core_result_schema_positive() {
4396 let schema = admit_final_schema(json!({
4397 "$schema": FINAL_JSON_SCHEMA_DIALECT,
4398 "type": "object",
4399 "properties": {
4400 "resultType": {"const": "complete"},
4401 "content": {"type": "array", "items": {"type": "string"}}
4402 },
4403 "required": ["resultType", "content"],
4404 "additionalProperties": false
4405 }))
4406 .expect("a strict final core result schema admits");
4407 let result = json!({"resultType": "complete", "content": ["ready"]});
4408
4409 validate_final_core_result(&schema, &result, FinalCoreResultType::Complete)
4410 .expect("the selected final core branch and schema both admit the result");
4411 assert_eq!(schema.schema()["$schema"], FINAL_JSON_SCHEMA_DIALECT);
4412 assert_eq!(FinalCoreResultType::Complete.as_str(), "complete");
4413 }
4414
4415 #[test]
4416 fn final_core_result_schema_cross_era_and_unknown_field_negatives() {
4417 let schema = admit_final_schema(json!({
4418 "$schema": FINAL_JSON_SCHEMA_DIALECT,
4419 "type": "object",
4420 "properties": {
4421 "resultType": {"const": "complete"},
4422 "content": {"type": "array", "items": {"type": "string"}}
4423 },
4424 "required": ["resultType", "content"],
4425 "additionalProperties": false
4426 }))
4427 .expect("the final schema admits");
4428 let accepted = json!({"resultType": "complete", "content": ["ready"]});
4429
4430 let mut cross_era = accepted.clone();
4431 cross_era["resultType"] = json!("legacy_complete");
4432 let cross_era_errors =
4433 validate_final_core_result(&schema, &cross_era, FinalCoreResultType::Complete)
4434 .expect_err("changing only resultType to a non-final branch must reject");
4435 assert!(cross_era_errors.iter().any(|error| {
4436 error.path == "root.resultType"
4437 && error.message
4438 == "resultType does not match the selected final core result branch"
4439 }));
4440
4441 let mut with_legacy_field = accepted.clone();
4442 with_legacy_field["protocolVersion"] = json!("2024-11-05");
4443 let unknown_field_errors =
4444 validate_final_core_result(&schema, &with_legacy_field, FinalCoreResultType::Complete)
4445 .expect_err("adding only a legacy result field must reject");
4446 assert!(unknown_field_errors.iter().any(|error| {
4447 error.path == "root"
4448 && error.message == "additional property not allowed: protocolVersion"
4449 }));
4450 assert_eq!(
4451 accepted,
4452 json!({"resultType": "complete", "content": ["ready"]})
4453 );
4454 }
4455
4456 fn bounded_draft_2020_12_schema() -> AdmittedSchema {
4457 admit_final_schema(json!({
4458 "$schema": FINAL_JSON_SCHEMA_DIALECT,
4459 "$defs": {
4460 "positive": {
4461 "allOf": [
4462 {"type": "integer"},
4463 {"minimum": 1}
4464 ]
4465 },
4466 "entry": {
4467 "type": "object",
4468 "properties": {
4469 "kind": {"enum": ["number", "label"]},
4470 "value": {}
4471 },
4472 "required": ["kind", "value"],
4473 "additionalProperties": false,
4474 "allOf": [{
4475 "if": {
4476 "properties": {"kind": {"const": "number"}},
4477 "required": ["kind"]
4478 },
4479 "then": {
4480 "properties": {"value": {"$ref": "#/$defs/positive"}}
4481 },
4482 "else": {
4483 "properties": {"value": {"type": "string", "minLength": 3}}
4484 }
4485 }]
4486 }
4487 },
4488 "type": "object",
4489 "properties": {
4490 "entries": {
4491 "type": "array",
4492 "items": {"$ref": "#/$defs/entry"}
4493 }
4494 },
4495 "required": ["entries"],
4496 "additionalProperties": false
4497 }))
4498 .expect("bounded local-reference schema admits")
4499 }
4500
4501 #[test]
4502 fn bounded_draft_2020_12_local_ref_composition_conditional_positive() {
4503 let schema = bounded_draft_2020_12_schema();
4504 let instance = json!({
4505 "entries": [
4506 {"kind": "number", "value": 7},
4507 {"kind": "label", "value": "ready"}
4508 ]
4509 });
4510
4511 schema
4512 .validate(&instance)
4513 .expect("local references, allOf, and the selected conditional branch validate");
4514 }
4515
4516 #[test]
4517 fn bounded_draft_2020_12_local_ref_composition_conditional_planted_negative() {
4518 let schema = bounded_draft_2020_12_schema();
4519 let accepted = json!({
4520 "entries": [
4521 {"kind": "number", "value": 7},
4522 {"kind": "label", "value": "ready"}
4523 ]
4524 });
4525 let mut planted = accepted.clone();
4526 planted["entries"][0]["value"] = json!(0);
4527
4528 let errors = schema.validate(&planted).expect_err(
4529 "changing only the local-reference value violates the selected then branch",
4530 );
4531 assert!(errors.iter().any(|error| {
4532 error.path == "root.entries[0].value" && error.message == "value must be >= 1"
4533 }));
4534 assert_eq!(
4535 accepted,
4536 json!({
4537 "entries": [
4538 {"kind": "number", "value": 7},
4539 {"kind": "label", "value": "ready"}
4540 ]
4541 })
4542 );
4543 }
4544
4545 fn bounded_draft_2020_12_unevaluated_properties_schema() -> AdmittedSchema {
4546 admit_final_schema(json!({
4547 "$schema": FINAL_JSON_SCHEMA_DIALECT,
4548 "type": "object",
4549 "allOf": [{
4550 "properties": {
4551 "label": {"type": "string"}
4552 },
4553 "required": ["label"]
4554 }],
4555 "unevaluatedProperties": false
4556 }))
4557 .expect("the bounded unevaluated-properties schema admits")
4558 }
4559
4560 #[test]
4561 fn bounded_draft_2020_12_unevaluated_properties_positive() {
4562 let schema = bounded_draft_2020_12_unevaluated_properties_schema();
4563 let accepted = json!({"label": "ready"});
4564
4565 schema
4566 .validate(&accepted)
4567 .expect("a property evaluated by a successful allOf branch remains accepted");
4568 assert_eq!(accepted, json!({"label": "ready"}));
4569 }
4570
4571 #[test]
4572 fn bounded_draft_2020_12_unevaluated_properties_planted_negative() {
4573 let schema = bounded_draft_2020_12_unevaluated_properties_schema();
4574 let accepted = json!({"label": "ready"});
4575 let mut planted = accepted.clone();
4576 planted["unexpected"] = json!(true);
4577
4578 let errors = schema
4579 .validate(&planted)
4580 .expect_err("adding only an unevaluated property must be rejected by the false schema");
4581 assert_eq!(errors.len(), 1);
4582 assert_eq!(errors[0].path, "root.unexpected");
4583 assert_eq!(errors[0].message, "schema rejects all values");
4584 assert_eq!(accepted, json!({"label": "ready"}));
4585 }
4586
4587 #[test]
4588 fn raw_validate_preserves_legacy_unevaluated_properties_behavior() {
4589 let legacy_schema = json!({
4590 "type": "object",
4591 "unevaluatedProperties": false
4592 });
4593 let legacy_instance = json!({"legacy": true});
4594
4595 assert!(validate(&legacy_schema, &legacy_instance).is_ok());
4596 assert_eq!(legacy_instance, json!({"legacy": true}));
4597 }
4598
4599 #[test]
4600 fn admitted_anchor_and_dynamic_reference_positive() {
4601 let anchored = admit_final_schema(json!({
4602 "$schema": FINAL_JSON_SCHEMA_DIALECT,
4603 "$defs": {
4604 "positive": {"$anchor": "positive", "type": "integer", "minimum": 1}
4605 },
4606 "$ref": "#positive"
4607 }))
4608 .expect("a local named anchor admits");
4609 anchored
4610 .validate(&json!(1))
4611 .expect("the named anchor resolves without external I/O");
4612
4613 let dynamic = admit_final_schema(json!({
4614 "$schema": FINAL_JSON_SCHEMA_DIALECT,
4615 "$defs": {
4616 "node": {
4617 "$dynamicAnchor": "node",
4618 "type": "object",
4619 "properties": {
4620 "value": {"type": "integer"},
4621 "child": {"$dynamicRef": "#node"}
4622 },
4623 "required": ["value"],
4624 "additionalProperties": false
4625 }
4626 },
4627 "$ref": "#node"
4628 }))
4629 .expect("a bounded recursive dynamic reference admits");
4630 let accepted = json!({"value": 1, "child": {"value": 2}});
4631 dynamic
4632 .validate(&accepted)
4633 .expect("a dynamic reference resolves to the active local anchor");
4634 assert_eq!(accepted, json!({"value": 1, "child": {"value": 2}}));
4635 }
4636
4637 #[test]
4638 fn admitted_anchor_and_dynamic_reference_planted_negative() {
4639 let schema = admit_final_schema(json!({
4640 "$schema": FINAL_JSON_SCHEMA_DIALECT,
4641 "$defs": {
4642 "node": {
4643 "$dynamicAnchor": "node",
4644 "type": "object",
4645 "properties": {
4646 "value": {"type": "integer"},
4647 "child": {"$dynamicRef": "#node"}
4648 },
4649 "required": ["value"],
4650 "additionalProperties": false
4651 }
4652 },
4653 "$ref": "#node"
4654 }))
4655 .expect("the recursive dynamic schema admits");
4656 let accepted = json!({"value": 1, "child": {"value": 2}});
4657 let mut planted = accepted.clone();
4658 planted["child"]["value"] = json!("not-an-integer");
4659
4660 let errors = schema
4661 .validate(&planted)
4662 .expect_err("changing only the dynamically referenced value must reject");
4663 assert!(errors.iter().any(|error| error.path == "root.child.value"));
4664 assert_eq!(accepted, json!({"value": 1, "child": {"value": 2}}));
4665 }
4666
4667 #[test]
4668 fn admitted_unevaluated_items_positive() {
4669 let schema = admit_final_schema(json!({
4670 "$schema": FINAL_JSON_SCHEMA_DIALECT,
4671 "type": "array",
4672 "prefixItems": [{"type": "string"}],
4673 "contains": {"type": "integer"},
4674 "unevaluatedItems": false
4675 }))
4676 .expect("the bounded unevaluated-items schema admits");
4677 let accepted = json!(["heading", 2]);
4678
4679 schema
4680 .validate(&accepted)
4681 .expect("prefixItems and contains annotations consume every item");
4682 assert_eq!(accepted, json!(["heading", 2]));
4683 }
4684
4685 #[test]
4686 fn admitted_unevaluated_items_planted_negative() {
4687 let schema = admit_final_schema(json!({
4688 "$schema": FINAL_JSON_SCHEMA_DIALECT,
4689 "type": "array",
4690 "prefixItems": [{"type": "string"}],
4691 "contains": {"type": "integer"},
4692 "unevaluatedItems": false
4693 }))
4694 .expect("the bounded unevaluated-items schema admits");
4695 let accepted = json!(["heading", 2]);
4696 let mut planted = accepted.clone();
4697 planted
4698 .as_array_mut()
4699 .expect("fixture is an array")
4700 .push(json!(true));
4701
4702 let errors = schema
4703 .validate(&planted)
4704 .expect_err("adding only an unevaluated item must reject");
4705 assert!(errors.iter().any(|error| error.path == "root[2]"));
4706 assert_eq!(accepted, json!(["heading", 2]));
4707 }
4708
4709 #[test]
4710 fn admitted_exact_numeric_boundaries_positive() {
4711 let minimum = admit_final_schema(json!({
4712 "$schema": FINAL_JSON_SCHEMA_DIALECT,
4713 "type": "integer",
4714 "minimum": 9007199254740993_u64
4715 }))
4716 .expect("the exact integer boundary schema admits");
4717 minimum
4718 .validate(&json!(9007199254740993_u64))
4719 .expect("the exact minimum itself is accepted");
4720
4721 let multiple = admit_final_schema(json!({
4722 "$schema": FINAL_JSON_SCHEMA_DIALECT,
4723 "type": "number",
4724 "multipleOf": 0.1
4725 }))
4726 .expect("the exact decimal divisor schema admits");
4727 multiple
4728 .validate(&json!(0.3))
4729 .expect("0.3 is exactly divisible by the decimal divisor 0.1");
4730 }
4731
4732 #[test]
4733 fn admitted_exact_numeric_boundaries_planted_negative() {
4734 let schema = admit_final_schema(json!({
4735 "$schema": FINAL_JSON_SCHEMA_DIALECT,
4736 "type": "integer",
4737 "minimum": 9007199254740993_u64
4738 }))
4739 .expect("the exact integer boundary schema admits");
4740 let accepted = json!(9007199254740993_u64);
4741 let planted = json!(9007199254740992_u64);
4742 let errors = schema
4743 .validate(&planted)
4744 .expect_err("changing only the integer below the exact boundary must reject");
4745 assert!(errors.iter().any(|error| error.path == "root"));
4746 assert_eq!(accepted, json!(9007199254740993_u64));
4747 }
4748
4749 #[test]
4750 fn admitted_final_string_lengths_above_u64_are_lossless_positive() {
4751 const BOUND: &str = "184467440737095516160e-1";
4752 let accepted_schema: Value = serde_json::from_str(&format!(
4753 r#"{{"$schema":"{FINAL_JSON_SCHEMA_DIALECT}","type":"string","minLength":1,"maxLength":{BOUND}}}"#
4754 ))
4755 .expect("the arbitrary-precision count schema parses");
4756 let schema = admit_final_schema(accepted_schema.clone())
4757 .expect("a mathematical string-length count above u64 admits");
4758 let accepted = json!("x");
4759
4760 schema
4761 .validate(&accepted)
4762 .expect("the exact upper string-length bound accepts the smaller instance");
4763 assert_eq!(
4764 schema.schema()["maxLength"]
4765 .as_number()
4766 .expect("admitted maxLength remains numeric")
4767 .as_str(),
4768 BOUND
4769 );
4770 assert_eq!(accepted_schema["minLength"], json!(1));
4771 assert_eq!(accepted, json!("x"));
4772 }
4773
4774 #[test]
4775 fn admitted_final_string_lengths_above_u64_are_lossless_planted_negative() {
4776 const BOUND: &str = "184467440737095516160e-1";
4777 let accepted_schema: Value = serde_json::from_str(&format!(
4778 r#"{{"$schema":"{FINAL_JSON_SCHEMA_DIALECT}","type":"string","minLength":1,"maxLength":{BOUND}}}"#
4779 ))
4780 .expect("the arbitrary-precision count schema parses");
4781 let mut planted_schema = accepted_schema.clone();
4782 planted_schema["minLength"] = planted_schema["maxLength"].clone();
4783 let accepted = json!("x");
4784
4785 let errors = admit_final_schema(planted_schema)
4786 .expect("the mathematical string-length lower bound admits")
4787 .validate(&accepted)
4788 .expect_err("changing only minLength to the exact large bound must reject");
4789 assert!(errors.iter().any(|error| {
4790 error.path == "root"
4791 && error.message == format!("string must be at least {BOUND} characters")
4792 }));
4793 assert_eq!(accepted_schema["minLength"], json!(1));
4794 assert_eq!(
4795 accepted_schema["maxLength"]
4796 .as_number()
4797 .expect("baseline maxLength remains numeric")
4798 .as_str(),
4799 BOUND
4800 );
4801 assert_eq!(accepted, json!("x"));
4802 }
4803
4804 #[test]
4805 fn admitted_final_item_counts_above_u64_are_lossless_positive() {
4806 const BOUND: &str = "184467440737095516160e-1";
4807 let accepted_schema: Value = serde_json::from_str(&format!(
4808 r#"{{"$schema":"{FINAL_JSON_SCHEMA_DIALECT}","type":"array","minItems":1,"maxItems":{BOUND}}}"#
4809 ))
4810 .expect("the arbitrary-precision count schema parses");
4811 let schema = admit_final_schema(accepted_schema.clone())
4812 .expect("a mathematical item count above u64 admits");
4813 let accepted = json!([null]);
4814
4815 schema
4816 .validate(&accepted)
4817 .expect("the exact upper item-count bound accepts the smaller instance");
4818 assert_eq!(
4819 schema.schema()["maxItems"]
4820 .as_number()
4821 .expect("admitted maxItems remains numeric")
4822 .as_str(),
4823 BOUND
4824 );
4825 assert_eq!(accepted_schema["minItems"], json!(1));
4826 assert_eq!(accepted, json!([null]));
4827 }
4828
4829 #[test]
4830 fn admitted_final_item_counts_above_u64_are_lossless_planted_negative() {
4831 const BOUND: &str = "184467440737095516160e-1";
4832 let accepted_schema: Value = serde_json::from_str(&format!(
4833 r#"{{"$schema":"{FINAL_JSON_SCHEMA_DIALECT}","type":"array","minItems":1,"maxItems":{BOUND}}}"#
4834 ))
4835 .expect("the arbitrary-precision count schema parses");
4836 let mut planted_schema = accepted_schema.clone();
4837 planted_schema["minItems"] = planted_schema["maxItems"].clone();
4838 let accepted = json!([null]);
4839
4840 let errors = admit_final_schema(planted_schema)
4841 .expect("the mathematical item-count lower bound admits")
4842 .validate(&accepted)
4843 .expect_err("changing only minItems to the exact large bound must reject");
4844 assert!(errors.iter().any(|error| {
4845 error.path == "root"
4846 && error.message == format!("array must have at least {BOUND} items")
4847 }));
4848 assert_eq!(accepted_schema["minItems"], json!(1));
4849 assert_eq!(
4850 accepted_schema["maxItems"]
4851 .as_number()
4852 .expect("baseline maxItems remains numeric")
4853 .as_str(),
4854 BOUND
4855 );
4856 assert_eq!(accepted, json!([null]));
4857 }
4858
4859 #[test]
4860 fn arbitrary_width_count_comparisons_are_final_only_and_raw_remains_legacy() {
4861 const BOUND: &str = "184467440737095516160e-1";
4862 let accepted_schema: Value = serde_json::from_str(&format!(
4863 r#"{{"$schema":"{FINAL_JSON_SCHEMA_DIALECT}","type":"array","minItems":1,"maxItems":{BOUND}}}"#
4864 ))
4865 .expect("the arbitrary-precision count schema parses");
4866 let mut planted_schema = accepted_schema.clone();
4867 planted_schema["minItems"] = planted_schema["maxItems"].clone();
4868 let instance = json!([null]);
4869
4870 validate(&planted_schema, &instance)
4871 .expect("raw validation retains its legacy u64-only count behavior");
4872 validate_strict(&planted_schema, &instance)
4873 .expect("raw strict validation retains its legacy u64-only count behavior");
4874 let errors = admit_final_schema(planted_schema)
4875 .expect("the arbitrary-width final count admits")
4876 .validate(&instance)
4877 .expect_err("the same final count remains mathematically enforced");
4878 assert!(errors.iter().any(|error| {
4879 error.path == "root"
4880 && error.message == format!("array must have at least {BOUND} items")
4881 }));
4882 assert_eq!(accepted_schema["minItems"], json!(1));
4883 assert_eq!(
4884 accepted_schema["maxItems"]
4885 .as_number()
4886 .expect("baseline maxItems remains numeric")
4887 .as_str(),
4888 BOUND
4889 );
4890 assert_eq!(instance, json!([null]));
4891 }
4892
4893 #[test]
4894 fn admitted_final_property_counts_above_u64_are_lossless_positive() {
4895 const BOUND: &str = "184467440737095516160e-1";
4896 let accepted_schema: Value = serde_json::from_str(&format!(
4897 r#"{{"$schema":"{FINAL_JSON_SCHEMA_DIALECT}","type":"object","minProperties":1,"maxProperties":{BOUND}}}"#
4898 ))
4899 .expect("the arbitrary-precision count schema parses");
4900 let schema = admit_final_schema(accepted_schema.clone())
4901 .expect("a mathematical property count above u64 admits");
4902 let accepted = json!({"ready": null});
4903
4904 schema
4905 .validate(&accepted)
4906 .expect("the exact upper property-count bound accepts the smaller instance");
4907 assert_eq!(
4908 schema.schema()["maxProperties"]
4909 .as_number()
4910 .expect("admitted maxProperties remains numeric")
4911 .as_str(),
4912 BOUND
4913 );
4914 assert_eq!(accepted_schema["minProperties"], json!(1));
4915 assert_eq!(accepted, json!({"ready": null}));
4916 }
4917
4918 #[test]
4919 fn admitted_final_property_counts_above_u64_are_lossless_planted_negative() {
4920 const BOUND: &str = "184467440737095516160e-1";
4921 let accepted_schema: Value = serde_json::from_str(&format!(
4922 r#"{{"$schema":"{FINAL_JSON_SCHEMA_DIALECT}","type":"object","minProperties":1,"maxProperties":{BOUND}}}"#
4923 ))
4924 .expect("the arbitrary-precision count schema parses");
4925 let mut planted_schema = accepted_schema.clone();
4926 planted_schema["minProperties"] = planted_schema["maxProperties"].clone();
4927 let accepted = json!({"ready": null});
4928
4929 let errors = admit_final_schema(planted_schema)
4930 .expect("the mathematical property-count lower bound admits")
4931 .validate(&accepted)
4932 .expect_err("changing only minProperties to the exact large bound must reject");
4933 assert!(errors.iter().any(|error| {
4934 error.path == "root"
4935 && error.message == format!("object must have at least {BOUND} properties")
4936 }));
4937 assert_eq!(accepted_schema["minProperties"], json!(1));
4938 assert_eq!(
4939 accepted_schema["maxProperties"]
4940 .as_number()
4941 .expect("baseline maxProperties remains numeric")
4942 .as_str(),
4943 BOUND
4944 );
4945 assert_eq!(accepted, json!({"ready": null}));
4946 }
4947
4948 #[test]
4949 fn admitted_final_contains_counts_above_u64_are_lossless_positive() {
4950 const BOUND: &str = "184467440737095516160e-1";
4951 let accepted_schema: Value = serde_json::from_str(&format!(
4952 r#"{{"$schema":"{FINAL_JSON_SCHEMA_DIALECT}","type":"array","contains":{{"const":"ready"}},"minContains":1,"maxContains":{BOUND},"unevaluatedItems":false}}"#
4953 ))
4954 .expect("the arbitrary-precision count schema parses");
4955 let schema = admit_final_schema(accepted_schema.clone())
4956 .expect("a mathematical contains count above u64 admits");
4957 let accepted = json!(["ready"]);
4958
4959 schema
4960 .validate(&accepted)
4961 .expect("the exact upper contains bound retains the successful item annotation");
4962 assert_eq!(
4963 schema.schema()["maxContains"]
4964 .as_number()
4965 .expect("admitted maxContains remains numeric")
4966 .as_str(),
4967 BOUND
4968 );
4969 assert_eq!(accepted_schema["minContains"], json!(1));
4970 assert_eq!(accepted, json!(["ready"]));
4971 }
4972
4973 #[test]
4974 fn admitted_final_contains_counts_above_u64_are_lossless_planted_negative() {
4975 const BOUND: &str = "184467440737095516160e-1";
4976 let accepted_schema: Value = serde_json::from_str(&format!(
4977 r#"{{"$schema":"{FINAL_JSON_SCHEMA_DIALECT}","type":"array","contains":{{"const":"ready"}},"minContains":1,"maxContains":{BOUND},"unevaluatedItems":false}}"#
4978 ))
4979 .expect("the arbitrary-precision count schema parses");
4980 let mut planted_schema = accepted_schema.clone();
4981 planted_schema["minContains"] = planted_schema["maxContains"].clone();
4982 let accepted = json!(["ready"]);
4983
4984 let errors = admit_final_schema(planted_schema)
4985 .expect("the mathematical contains lower bound admits")
4986 .validate(&accepted)
4987 .expect_err("changing only minContains to the exact large bound must reject");
4988 assert!(errors.iter().any(|error| {
4989 error.path == "root"
4990 && error.message == format!("array must contain at least {BOUND} matching items")
4991 }));
4992 assert_eq!(accepted_schema["minContains"], json!(1));
4993 assert_eq!(
4994 accepted_schema["maxContains"]
4995 .as_number()
4996 .expect("baseline maxContains remains numeric")
4997 .as_str(),
4998 BOUND
4999 );
5000 assert_eq!(accepted, json!(["ready"]));
5001 }
5002
5003 #[test]
5004 fn admitted_underscore_anchor_positive() {
5005 let schema = admit_final_schema(json!({
5006 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5007 "$defs": {
5008 "private": {"$anchor": "_private", "const": "ready"}
5009 },
5010 "$ref": "#_private"
5011 }))
5012 .expect("an underscore-prefixed JSON Schema anchor admits");
5013 let accepted = json!("ready");
5014
5015 schema
5016 .validate(&accepted)
5017 .expect("the underscore-prefixed local anchor resolves");
5018 assert_eq!(accepted, json!("ready"));
5019 }
5020
5021 #[test]
5022 fn admitted_underscore_anchor_planted_negative() {
5023 let accepted = json!({
5024 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5025 "$defs": {
5026 "private": {"$anchor": "_private", "const": "ready"}
5027 }
5028 });
5029 admit_final_schema(accepted.clone())
5030 .expect("the underscore-prefixed anchor remains the valid baseline");
5031 let mut planted = accepted.clone();
5032 planted["$defs"]["private"]["$anchor"] = json!("-private");
5033
5034 let error = admit_final_schema(planted)
5035 .expect_err("changing only the initial anchor character to a hyphen must reject");
5036 assert_eq!(error.path(), "$.$defs.private.$anchor");
5037 assert_eq!(error.reason(), "schema anchor has an invalid name");
5038 assert_eq!(accepted["$defs"]["private"]["$anchor"], json!("_private"));
5039 }
5040
5041 #[test]
5042 fn admitted_unique_items_uses_numeric_schema_equality_positive() {
5043 let schema = admit_final_schema(json!({
5044 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5045 "type": "array",
5046 "uniqueItems": true
5047 }))
5048 .expect("the final unique-items schema admits");
5049 let accepted = json!([1, 2.0]);
5050
5051 schema
5052 .validate(&accepted)
5053 .expect("numerically distinct items remain unique");
5054 assert_eq!(accepted, json!([1, 2.0]));
5055 }
5056
5057 #[test]
5058 fn admitted_unique_items_uses_numeric_schema_equality_planted_negative() {
5059 let schema = admit_final_schema(json!({
5060 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5061 "type": "array",
5062 "uniqueItems": true
5063 }))
5064 .expect("the final unique-items schema admits");
5065 let accepted = json!([1, 2.0]);
5066 let mut planted = accepted.clone();
5067 planted[1] = json!(1.0);
5068
5069 let errors = schema
5070 .validate(&planted)
5071 .expect_err("changing only 2.0 to the numerically equal 1.0 must reject");
5072 assert!(errors.iter().any(|error| {
5073 error.path == "root[1]" && error.message == "duplicate item in array"
5074 }));
5075 assert_eq!(accepted, json!([1, 2.0]));
5076 }
5077
5078 fn arbitrary_precision_number(decimal_digits: usize) -> Value {
5079 serde_json::from_str(&"1".repeat(decimal_digits))
5080 .expect("the workspace serde_json configuration retains arbitrary-precision numbers")
5081 }
5082
5083 #[test]
5084 fn admitted_const_and_enum_exact_equality_bound_positive() {
5085 let number = arbitrary_precision_number(MAX_EXACT_DECIMAL_DIGITS);
5086 let const_schema = admit_final_schema(json!({
5087 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5088 "const": number.clone()
5089 }))
5090 .expect("a const at the exact numeric equality bound admits");
5091 const_schema
5092 .validate(&number)
5093 .expect("an admitted const remains reflexive at the exact bound");
5094
5095 let enum_schema = admit_final_schema(json!({
5096 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5097 "enum": [number.clone()]
5098 }))
5099 .expect("an enum member at the exact numeric equality bound admits");
5100 enum_schema
5101 .validate(&number)
5102 .expect("an admitted enum member remains reflexive at the exact bound");
5103 }
5104
5105 #[test]
5106 fn admitted_const_and_enum_exact_equality_bound_planted_negative() {
5107 let accepted = arbitrary_precision_number(MAX_EXACT_DECIMAL_DIGITS);
5108 let planted = arbitrary_precision_number(MAX_EXACT_DECIMAL_DIGITS + 1);
5109
5110 let const_error = admit_final_schema(json!({
5111 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5112 "const": planted.clone()
5113 }))
5114 .expect_err("adding one digit beyond the const equality bound must reject admission");
5115 assert_eq!(const_error.path(), "$.const");
5116 assert_eq!(
5117 const_error.reason(),
5118 "const or enum value exceeds exact numeric equality bound"
5119 );
5120
5121 let enum_error = admit_final_schema(json!({
5122 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5123 "enum": [planted]
5124 }))
5125 .expect_err("adding one digit beyond the enum equality bound must reject admission");
5126 assert_eq!(enum_error.path(), "$.enum[0]");
5127 assert_eq!(
5128 enum_error.reason(),
5129 "const or enum value exceeds exact numeric equality bound"
5130 );
5131 assert_eq!(accepted.to_string().len(), MAX_EXACT_DECIMAL_DIGITS);
5132 }
5133
5134 #[test]
5135 fn admitted_multiple_of_work_accounting_positive() {
5136 let schema = admit_final_schema(json!({
5137 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5138 "type": "number",
5139 "multipleOf": 1
5140 }))
5141 .expect("the exact divisor schema admits");
5142 let accepted = arbitrary_precision_number((MAX_SCHEMA_VALIDATION_WORK - 1) / 4);
5143
5144 schema
5145 .validate(&accepted)
5146 .expect("the exact long-division work budget admits its final charged digit");
5147 assert_eq!(
5148 accepted.to_string().len(),
5149 (MAX_SCHEMA_VALIDATION_WORK - 1) / 4
5150 );
5151 }
5152
5153 #[test]
5154 fn admitted_multiple_of_work_accounting_planted_negative() {
5155 let schema = admit_final_schema(json!({
5156 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5157 "type": "number",
5158 "multipleOf": 1
5159 }))
5160 .expect("the exact divisor schema admits");
5161 let accepted = arbitrary_precision_number((MAX_SCHEMA_VALIDATION_WORK - 1) / 4);
5162 let planted = arbitrary_precision_number(((MAX_SCHEMA_VALIDATION_WORK - 1) / 4) + 1);
5163
5164 let errors = schema
5165 .validate(&planted)
5166 .expect_err("adding one decimal digit beyond the charged division budget must reject");
5167 assert!(
5168 errors
5169 .iter()
5170 .any(|error| error.message == "schema validation work limit exceeded")
5171 );
5172 assert_eq!(
5173 accepted.to_string().len(),
5174 (MAX_SCHEMA_VALIDATION_WORK - 1) / 4
5175 );
5176 }
5177
5178 #[test]
5179 fn admitted_schema_refuses_unknown_vocabulary_keywords_and_raw_semantics_remain_legacy() {
5180 let error = admit_final_schema(json!({
5181 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5182 "unsupportedFinalKeyword": true
5183 }))
5184 .expect_err("unsupported vocabularies fail before final-schema validation");
5185 assert_eq!(error.path(), "$.unsupportedFinalKeyword");
5186 assert_eq!(
5187 error.reason(),
5188 "unsupported Draft 2020-12 vocabulary keyword"
5189 );
5190
5191 assert!(validate(&json!({"type": "integer"}), &json!(1.0)).is_err());
5192 assert!(validate(&json!({"$ref": "#named"}), &json!(true)).is_err());
5193 }
5194
5195 #[test]
5196 fn admitted_schema_accepts_declared_vocabulary_local_resources_and_content_annotations() {
5197 let schema = admit_final_schema(json!({
5198 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5199 "$id": "https://schemas.example.test/root",
5200 "$vocabulary": {
5201 CORE_VOCABULARY_URI: true,
5202 APPLICATOR_VOCABULARY_URI: true,
5203 VALIDATION_VOCABULARY_URI: true,
5204 CONTENT_VOCABULARY_URI: true,
5205 "urn:example:optional-extension": false
5206 },
5207 "type": "object",
5208 "properties": {
5209 "payload": {"$ref": "https://schemas.example.test/payload"}
5210 },
5211 "required": ["payload"],
5212 "$defs": {
5213 "payload": {
5214 "$id": "https://schemas.example.test/payload",
5215 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5216 "$vocabulary": {
5217 CORE_VOCABULARY_URI: true,
5218 CONTENT_VOCABULARY_URI: true
5219 },
5220 "type": "string",
5221 "contentEncoding": "base64",
5222 "contentMediaType": "application/json",
5223 "contentSchema": {
5224 "type": "object",
5225 "required": ["ok"],
5226 "properties": {"ok": {"type": "boolean"}}
5227 }
5228 }
5229 }
5230 }))
5231 .expect("declared supported vocabularies and local resources admit");
5232
5233 schema
5234 .validate(&json!({"payload": "this is deliberately not base64"}))
5235 .expect("content annotations do not decode or validate instance strings");
5236 }
5237
5238 #[test]
5239 fn admitted_schema_rejects_duplicate_and_undeclared_local_resource_ids() {
5240 let accepted = json!({
5241 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5242 "$id": "https://schemas.example.test/root",
5243 "$defs": {
5244 "payload": {
5245 "$id": "https://schemas.example.test/payload",
5246 "type": "string"
5247 },
5248 "other": {
5249 "$id": "https://schemas.example.test/other",
5250 "type": "string"
5251 }
5252 },
5253 "$ref": "https://schemas.example.test/payload"
5254 });
5255 admit_final_schema(accepted.clone())
5256 .expect("a unique declared resource satisfies an absolute local reference");
5257
5258 let mut duplicate = accepted.clone();
5259 duplicate["$defs"]["other"]["$id"] = json!("https://schemas.example.test/root");
5260 let duplicate_error = admit_final_schema(duplicate)
5261 .expect_err("changing only the resource id to a duplicate rejects");
5262 assert_eq!(duplicate_error.path(), "$.$defs.other.$id");
5263 assert_eq!(
5264 duplicate_error.reason(),
5265 "duplicate local schema resource identifier"
5266 );
5267
5268 let mut undeclared = accepted;
5269 undeclared["$ref"] = json!("https://schemas.example.test/not-declared");
5270 let reference_error = admit_final_schema(undeclared)
5271 .expect_err("undeclared URI references cannot trigger retrieval");
5272 assert_eq!(reference_error.path(), "$.$ref");
5273 assert_eq!(
5274 reference_error.reason(),
5275 "external schema reference is not allowed"
5276 );
5277 }
5278
5279 #[test]
5280 fn admitted_schema_rejects_invalid_resource_id_with_a_near_identical_fixture() {
5281 let accepted = json!({
5282 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5283 "$id": "urn:example:bounded-schema",
5284 "type": "string"
5285 });
5286 admit_final_schema(accepted.clone()).expect("a bounded absolute resource id admits");
5287
5288 let mut planted = accepted;
5289 planted["$id"] = json!("relative-schema");
5290 let error = admit_final_schema(planted)
5291 .expect_err("changing only the id to a relative URI rejects");
5292 assert_eq!(error.path(), "$.$id");
5293 assert_eq!(
5294 error.reason(),
5295 "schema $id must resolve to a bounded absolute URI"
5296 );
5297
5298 let fragmented = json!({
5299 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5300 "$id": "urn:example:bounded-schema#fragment",
5301 "type": "string"
5302 });
5303 let fragment_error =
5304 admit_final_schema(fragmented).expect_err("changing only the id fragment rejects");
5305 assert_eq!(fragment_error.path(), "$.$id");
5306 assert_eq!(
5307 fragment_error.reason(),
5308 "schema $id must not contain a fragment"
5309 );
5310 }
5311
5312 #[test]
5313 fn ordinary_schema_vocabulary_is_ignored_and_meta_schema_vocabulary_is_a_dialect_gate() {
5314 let accepted = json!({
5315 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5316 "$vocabulary": {
5317 "not-a-uri": "not-a-boolean"
5318 },
5319 "type": "string"
5320 });
5321 admit_final_schema(accepted)
5322 .expect("ordinary-schema $vocabulary does not select or gate a dialect");
5323
5324 let meta_schema = json!({
5325 "$id": "https://schemas.example.test/meta",
5326 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5327 "$vocabulary": {
5328 CORE_VOCABULARY_URI: true,
5329 "urn:example:unsupported": true
5330 }
5331 });
5332 let schema = json!({
5333 "$id": "https://schemas.example.test/root",
5334 "$schema": "https://schemas.example.test/meta",
5335 "$defs": {"meta": meta_schema},
5336 "type": "string"
5337 });
5338 let error = admit_final_schema(schema).expect_err(
5339 "only a selected meta-schema vocabulary can reject an unsupported requirement",
5340 );
5341 assert_eq!(error.path(), "$.$vocabulary.urn:example:unsupported");
5342 assert_eq!(
5343 error.reason(),
5344 "required schema vocabulary is not supported"
5345 );
5346 }
5347
5348 #[test]
5349 fn schema_dialect_must_be_absolute_with_a_one_value_negative() {
5350 let accepted = json!({
5351 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5352 "type": "string"
5353 });
5354 admit_final_schema(accepted.clone()).expect("the canonical absolute dialect admits");
5355
5356 let mut planted = accepted.clone();
5357 planted["$schema"] = json!("meta/validation");
5358 let error = admit_final_schema(planted)
5359 .expect_err("changing only the dialect to a relative identifier must reject");
5360 assert_eq!(error.path(), "$.$schema");
5361 assert_eq!(
5362 error.reason(),
5363 "schema dialect must be a bounded absolute URI"
5364 );
5365 assert_eq!(accepted["$schema"], json!(FINAL_JSON_SCHEMA_DIALECT));
5366 }
5367
5368 #[test]
5369 fn admitted_schema_scopes_relative_ids_anchors_and_inner_local_references() {
5370 let accepted = json!({
5371 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5372 "$id": "https://schemas.example.test/catalog/root.json",
5373 "$anchor": "outer",
5374 "type": "object",
5375 "properties": {
5376 "outer": {"$ref": "resources/outer.json#outer"}
5377 },
5378 "required": ["outer"],
5379 "$defs": {
5380 "outer": {
5381 "$id": "resources/outer.json",
5382 "$anchor": "outer",
5383 "type": "object",
5384 "properties": {
5385 "value": {"$ref": "#integer"},
5386 "nested": {"$ref": "nested.json#nested"}
5387 },
5388 "required": ["value", "nested"],
5389 "$defs": {
5390 "integer": {"$anchor": "integer", "type": "integer"},
5391 "nested": {
5392 "$id": "nested.json",
5393 "$anchor": "nested",
5394 "type": "string"
5395 }
5396 }
5397 }
5398 }
5399 });
5400 let schema = admit_final_schema(accepted.clone())
5401 .expect("relative ids and resource-scoped anchors admit");
5402 schema
5403 .validate(&json!({"outer": {"value": 7, "nested": "ready"}}))
5404 .expect("inner local and relative references resolve from the owning resource");
5405
5406 let mut planted = accepted;
5407 planted["$defs"]["outer"]["properties"]["value"]["$ref"] = json!("#root-only");
5408 let error = admit_final_schema(planted)
5409 .expect_err("changing only an inner reference to an outer anchor rejects");
5410 assert_eq!(error.path(), "$.$defs.outer.properties.value.$ref");
5411 assert_eq!(error.reason(), "unresolved local schema reference");
5412 }
5413
5414 #[test]
5415 fn relative_resource_ids_resolve_from_authority_only_and_query_bases() {
5416 for base in [
5417 "https://schemas.example.test",
5418 "https://schemas.example.test?catalog=bounded",
5419 ] {
5420 let schema = admit_final_schema(json!({
5421 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5422 "$id": base,
5423 "$defs": {
5424 "child": {
5425 "$id": "child.json",
5426 "type": "string",
5427 "const": "ready"
5428 }
5429 },
5430 "$ref": "child.json"
5431 }))
5432 .expect("a relative child resource resolves below an authority-only base");
5433 schema
5434 .validate(&json!("ready"))
5435 .expect("the authority-relative child reference reaches its local resource");
5436 }
5437 }
5438
5439 #[test]
5440 fn rfc3986_reference_resolution_table_preserves_network_query_and_dot_boundaries() {
5441 let vectors = [
5442 (
5443 "https://schemas.example.test/a/b?base=1",
5444 "https://schemas.example.test/a//b?absolute=1",
5445 "https://schemas.example.test/a//b?absolute=1",
5446 ),
5447 (
5448 "https://schemas.example.test/a/b?base=1",
5449 "/a//b",
5450 "https://schemas.example.test/a//b",
5451 ),
5452 (
5453 "https://schemas.example.test/a/b?base=1",
5454 "/a/.",
5455 "https://schemas.example.test/a/",
5456 ),
5457 (
5458 "https://schemas.example.test/a/b?base=1",
5459 "/a/..",
5460 "https://schemas.example.test/",
5461 ),
5462 (
5463 "https://schemas.example.test/a/b?base=1",
5464 "child/./leaf?relative=1",
5465 "https://schemas.example.test/a/child/leaf?relative=1",
5466 ),
5467 (
5468 "https://schemas.example.test/a/b?base=1",
5469 "child/..",
5470 "https://schemas.example.test/a/",
5471 ),
5472 (
5473 "https://schemas.example.test/a/b?base=1",
5474 "//authority.example.test/a//b?network=1",
5475 "https://authority.example.test/a//b?network=1",
5476 ),
5477 (
5478 "https://schemas.example.test/a/b?base=1",
5479 "//authority.example.test/a/.",
5480 "https://authority.example.test/a/",
5481 ),
5482 (
5483 "https://schemas.example.test/a/b?base=1",
5484 "//authority.example.test/a/..",
5485 "https://authority.example.test/",
5486 ),
5487 (
5488 "https://schemas.example.test/a/b?base=1",
5489 "?next=1",
5490 "https://schemas.example.test/a/b?next=1",
5491 ),
5492 (
5493 "https://schemas.example.test/a/b?base=1",
5494 "",
5495 "https://schemas.example.test/a/b?base=1",
5496 ),
5497 ];
5498
5499 for (base, reference, expected) in vectors {
5500 assert_eq!(
5501 resolve_uri_reference(Some(base), reference).as_deref(),
5502 Some(expected),
5503 "RFC 3986 resolution must retain the boundary semantics of {reference:?}"
5504 );
5505 }
5506 }
5507
5508 #[test]
5509 fn rfc3986_remove_dot_segments_table_preserves_empty_and_trailing_segments() {
5510 let vectors = [
5511 ("/a/b/c/./../../g", "/a/g"),
5512 ("mid/content=5/../6", "mid/6"),
5513 ("/a//b", "/a//b"),
5514 ("/a/.", "/a/"),
5515 ("/a/..", "/"),
5516 ("/../", "/"),
5517 ("../g", "g"),
5518 ("g/./h", "g/h"),
5519 ];
5520
5521 for (path, expected) in vectors {
5522 assert_eq!(
5523 remove_uri_dot_segments(path),
5524 expected,
5525 "RFC 3986 section 5.2.4 preserves the path boundary semantics of {path:?}"
5526 );
5527 }
5528 }
5529
5530 #[test]
5531 fn rfc3986_dot_segment_one_value_negatives_do_not_select_adjacent_resources() {
5532 let cases = [
5533 ("/a//b", "https://schemas.example.test/a//b", "/a/b"),
5534 ("/a/.", "https://schemas.example.test/a/", "/a"),
5535 ("/a/..", "https://schemas.example.test/", "/a"),
5536 ];
5537
5538 for (reference, resource_id, planted_reference) in cases {
5539 let schema = json!({
5540 "$id": "https://schemas.example.test/root.json",
5541 "$defs": {
5542 "target": {"$id": resource_id, "const": "ready"}
5543 }
5544 });
5545 let root = schema
5546 .as_object()
5547 .expect("the dot-segment fixture remains a schema object");
5548
5549 let target = resolve_local_reference(&schema, root, reference)
5550 .expect("the RFC 3986 vector reaches its declared local resource");
5551 assert_eq!(target, &schema["$defs"]["target"]);
5552 assert_eq!(
5553 resolve_local_reference(&schema, root, planted_reference),
5554 Err("external schema reference is not allowed"),
5555 "changing only the path boundary must not select {resource_id:?}"
5556 );
5557 }
5558 }
5559
5560 #[test]
5561 fn final_local_reference_table_preserves_network_query_and_fragment_boundaries() {
5562 let schema = json!({
5563 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5564 "$id": "https://schemas.example.test/root?base=1",
5565 "$anchor": "root",
5566 "$defs": {
5567 "query": {
5568 "$id": "?selected=1",
5569 "$anchor": "selected",
5570 "const": "ready"
5571 },
5572 "network": {
5573 "$id": "https://authority.example.test/a//b?network=1",
5574 "$anchor": "network",
5575 "const": "network"
5576 }
5577 }
5578 });
5579 let root = schema
5580 .as_object()
5581 .expect("the RFC boundary fixture remains a schema object");
5582 let vectors = [
5583 ("#root", "root"),
5584 ("?selected=1#selected", "query"),
5585 ("//authority.example.test/a//b?network=1#network", "network"),
5586 ];
5587
5588 for (reference, expected_definition) in vectors {
5589 let target = resolve_local_reference(&schema, root, reference)
5590 .expect("the table vector resolves inside the local schema catalog");
5591 assert_eq!(
5592 target,
5593 if expected_definition == "root" {
5594 &schema
5595 } else {
5596 &schema["$defs"][expected_definition]
5597 },
5598 "the URI identifier and fragment boundaries select the expected resource for {reference:?}"
5599 );
5600 }
5601
5602 let accepted = json!({
5603 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5604 "$id": "https://schemas.example.test/root?base=1",
5605 "$defs": {"query": schema["$defs"]["query"].clone()},
5606 "$ref": "?selected=1#selected"
5607 });
5608 admit_final_schema(accepted.clone())
5609 .expect("the query and fragment boundary fixture admits")
5610 .validate(&json!("ready"))
5611 .expect("the query-selected anchored resource validates");
5612
5613 let mut planted = accepted.clone();
5614 planted["$ref"] = json!("?selected=2#selected");
5615 let error = admit_final_schema(planted)
5616 .expect_err("changing only the query value must not select the declared resource");
5617 assert_eq!(error.path(), "$.$ref");
5618 assert_eq!(error.reason(), "external schema reference is not allowed");
5619 assert_eq!(accepted["$ref"], json!("?selected=1#selected"));
5620 }
5621
5622 #[test]
5623 fn absolute_references_normalize_dot_segments_with_a_one_value_negative() {
5624 let accepted = json!({
5625 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5626 "$id": "https://schemas.example.test/catalog/root.json",
5627 "$defs": {
5628 "child": {
5629 "$id": "../child.json",
5630 "const": "ready"
5631 }
5632 },
5633 "$ref": "https://schemas.example.test/catalog/../child.json"
5634 });
5635 let schema = admit_final_schema(accepted.clone())
5636 .expect("an absolute reference resolves after dot-segment normalization");
5637 schema
5638 .validate(&json!("ready"))
5639 .expect("the normalized absolute reference reaches the local child resource");
5640
5641 let mut planted = accepted.clone();
5642 planted["$ref"] = json!("https://schemas.example.test/catalog/../missing.json");
5643 let error = admit_final_schema(planted)
5644 .expect_err("changing only the normalized target to an undeclared resource rejects");
5645 assert_eq!(error.path(), "$.$ref");
5646 assert_eq!(error.reason(), "external schema reference is not allowed");
5647 assert_eq!(
5648 accepted["$ref"],
5649 json!("https://schemas.example.test/catalog/../child.json")
5650 );
5651 }
5652
5653 #[test]
5654 fn final_references_percent_decode_before_json_pointer_unescaping() {
5655 let accepted = json!({
5656 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5657 "$defs": {
5658 "a/b": {"const": "ready"}
5659 },
5660 "$ref": "#/$defs/a%7E1b"
5661 });
5662 let schema = admit_final_schema(accepted.clone())
5663 .expect("percent decoding before ~1 decoding reaches the slash-containing definition");
5664 schema
5665 .validate(&json!("ready"))
5666 .expect("the percent-encoded pointer token resolves");
5667
5668 let mut invalid_percent = accepted.clone();
5669 invalid_percent["$ref"] = json!("#/$defs/a%7G1b");
5670 let invalid_percent_error = admit_final_schema(invalid_percent)
5671 .expect_err("changing only one percent-escape nibble must reject");
5672 assert_eq!(invalid_percent_error.path(), "$.$ref");
5673 assert_eq!(
5674 invalid_percent_error.reason(),
5675 "invalid local schema reference"
5676 );
5677
5678 let mut invalid_utf8 = accepted.clone();
5679 invalid_utf8["$ref"] = json!("#/$defs/a%FF1b");
5680 let invalid_utf8_error = admit_final_schema(invalid_utf8)
5681 .expect_err("changing only the pointer escape to invalid UTF-8 must reject");
5682 assert_eq!(invalid_utf8_error.path(), "$.$ref");
5683 assert_eq!(
5684 invalid_utf8_error.reason(),
5685 "invalid local schema reference"
5686 );
5687 assert_eq!(accepted["$ref"], json!("#/$defs/a%7E1b"));
5688 }
5689
5690 #[test]
5691 fn raw_validation_retains_its_legacy_local_reference_behavior() {
5692 let legacy = json!({
5693 "$defs": {
5694 "ready": {"const": "ready"},
5695 "a/b": {"const": "ready"}
5696 },
5697 "$ref": "#/$defs/ready"
5698 });
5699 validate(&legacy, &json!("ready"))
5700 .expect("the raw validator retains ordinary legacy JSON Pointer resolution");
5701
5702 let mut percent_encoded = legacy.clone();
5703 percent_encoded["$ref"] = json!("#/$defs/a%7E1b");
5704 let errors = validate(&percent_encoded, &json!("ready"))
5705 .expect_err("raw validation does not acquire final-schema percent decoding semantics");
5706 assert_eq!(errors[0].message, "unresolved local schema reference");
5707 assert_eq!(legacy["$ref"], json!("#/$defs/ready"));
5708 }
5709
5710 #[test]
5711 fn local_resource_resolution_charges_exact_shared_work_units() {
5712 let schema = json!({
5713 "$id": "https://schemas.example.test/root.json",
5714 "properties": {
5715 "source": {"$ref": "child.json"}
5716 },
5717 "$defs": {
5718 "target": {
5719 "$id": "child.json",
5720 "const": "ready"
5721 }
5722 }
5723 });
5724 let source = schema["properties"]["source"]
5725 .as_object()
5726 .expect("the source fixture remains a schema object");
5727
5728 let mut exact_context = ValidationContext::new(&schema, true);
5729 exact_context.remaining_work = 5;
5730 let target =
5731 resolve_local_reference_with_work(&schema, source, "child.json", &mut exact_context)
5732 .expect("two scope visits and three resource visits fit the exact work budget");
5733 assert_eq!(target, &json!({"$id": "child.json", "const": "ready"}));
5734 assert_eq!(exact_context.remaining_work, 0);
5735
5736 let mut planted_context = ValidationContext::new(&schema, true);
5737 planted_context.remaining_work = 4;
5738 let error =
5739 resolve_local_reference_with_work(&schema, source, "child.json", &mut planted_context)
5740 .expect_err("removing one lookup work unit must reject before the target is found");
5741 assert_eq!(error, "schema validation work limit exceeded");
5742 assert_eq!(planted_context.remaining_work, 0);
5743 }
5744
5745 #[test]
5746 fn admitted_schema_content_annotations_have_bounded_positive_and_negative_cases() {
5747 let accepted = json!({
5748 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5749 "type": "string",
5750 "contentEncoding": "base64",
5751 "contentMediaType": "application/json",
5752 "contentSchema": {"type": "object"}
5753 });
5754 admit_final_schema(accepted.clone()).expect("bounded content annotations admit");
5755
5756 let mut encoding = accepted.clone();
5757 encoding["contentEncoding"] = json!(true);
5758 let encoding_error =
5759 admit_final_schema(encoding).expect_err("changing only contentEncoding rejects");
5760 assert_eq!(encoding_error.path(), "$.contentEncoding");
5761 assert_eq!(
5762 encoding_error.reason(),
5763 "content annotation keyword must be a bounded non-empty string"
5764 );
5765
5766 let mut media_type = accepted.clone();
5767 media_type["contentMediaType"] = json!(true);
5768 let media_type_error =
5769 admit_final_schema(media_type).expect_err("changing only contentMediaType rejects");
5770 assert_eq!(media_type_error.path(), "$.contentMediaType");
5771 assert_eq!(
5772 media_type_error.reason(),
5773 "content annotation keyword must be a bounded non-empty string"
5774 );
5775
5776 let mut empty_encoding = accepted.clone();
5777 empty_encoding["contentEncoding"] = json!("");
5778 let empty_encoding_error = admit_final_schema(empty_encoding)
5779 .expect_err("changing only contentEncoding to empty rejects");
5780 assert_eq!(empty_encoding_error.path(), "$.contentEncoding");
5781 assert_eq!(
5782 empty_encoding_error.reason(),
5783 "content annotation keyword must be a bounded non-empty string"
5784 );
5785
5786 let mut content_schema = accepted;
5787 content_schema["contentSchema"] = json!("not-a-schema");
5788 let content_schema_error =
5789 admit_final_schema(content_schema).expect_err("changing only contentSchema rejects");
5790 assert_eq!(content_schema_error.path(), "$.contentSchema");
5791 assert_eq!(
5792 content_schema_error.reason(),
5793 "schema must be an object or boolean"
5794 );
5795 }
5796
5797 fn assert_admitted_annotations_accept_only_evaluated_members(schema: Value, accepted: Value) {
5798 let schema = admit_final_schema(schema).expect("the bounded annotation schema admits");
5799 schema
5800 .validate(&accepted)
5801 .expect("members annotated by successful applicators remain accepted");
5802
5803 let mut planted = accepted.clone();
5804 planted
5805 .as_object_mut()
5806 .expect("the annotation fixture is an object")
5807 .insert("unexpected".to_owned(), Value::Bool(true));
5808 let errors = schema
5809 .validate(&planted)
5810 .expect_err("adding only an unevaluated member must reject");
5811 assert!(errors.iter().any(|error| {
5812 error.path == "root.unexpected" && error.message == "schema rejects all values"
5813 }));
5814 assert!(accepted.get("unexpected").is_none());
5815 }
5816
5817 #[test]
5818 fn admitted_ref_annotations_reach_unevaluated_properties() {
5819 assert_admitted_annotations_accept_only_evaluated_members(
5820 json!({
5821 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5822 "$defs": {
5823 "referenced": {
5824 "properties": {"viaRef": {"type": "integer"}},
5825 "required": ["viaRef"]
5826 }
5827 },
5828 "$ref": "#/$defs/referenced",
5829 "unevaluatedProperties": false
5830 }),
5831 json!({"viaRef": 1}),
5832 );
5833 }
5834
5835 #[test]
5836 fn admitted_all_of_annotations_reach_unevaluated_properties() {
5837 assert_admitted_annotations_accept_only_evaluated_members(
5838 json!({
5839 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5840 "allOf": [{
5841 "properties": {"viaAllOf": {"type": "integer"}},
5842 "required": ["viaAllOf"]
5843 }],
5844 "unevaluatedProperties": false
5845 }),
5846 json!({"viaAllOf": 1}),
5847 );
5848 }
5849
5850 #[test]
5851 fn admitted_any_of_unions_all_successful_branch_annotations() {
5852 assert_admitted_annotations_accept_only_evaluated_members(
5853 json!({
5854 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5855 "anyOf": [
5856 {
5857 "properties": {"alpha": {"type": "integer"}},
5858 "required": ["alpha"]
5859 },
5860 {
5861 "properties": {"beta": {"type": "integer"}},
5862 "required": ["beta"]
5863 }
5864 ],
5865 "unevaluatedProperties": false
5866 }),
5867 json!({"alpha": 1, "beta": 2}),
5868 );
5869 }
5870
5871 #[test]
5872 fn admitted_one_of_uses_only_the_unique_successful_branch_annotations() {
5873 assert_admitted_annotations_accept_only_evaluated_members(
5874 json!({
5875 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5876 "oneOf": [
5877 {
5878 "properties": {"alpha": {"type": "integer"}},
5879 "required": ["alpha"]
5880 },
5881 {
5882 "properties": {"beta": {"type": "integer"}},
5883 "required": ["beta"]
5884 }
5885 ],
5886 "unevaluatedProperties": false
5887 }),
5888 json!({"alpha": 1}),
5889 );
5890 }
5891
5892 #[test]
5893 fn admitted_dependent_schema_annotations_reach_unevaluated_properties() {
5894 assert_admitted_annotations_accept_only_evaluated_members(
5895 json!({
5896 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5897 "properties": {"trigger": {"const": true}},
5898 "required": ["trigger"],
5899 "dependentSchemas": {
5900 "trigger": {
5901 "properties": {"payload": {"type": "string"}},
5902 "required": ["payload"]
5903 }
5904 },
5905 "unevaluatedProperties": false
5906 }),
5907 json!({"trigger": true, "payload": "ready"}),
5908 );
5909 }
5910
5911 #[test]
5912 fn admitted_if_without_then_or_else_propagates_successful_annotations() {
5913 assert_admitted_annotations_accept_only_evaluated_members(
5914 json!({
5915 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5916 "if": {
5917 "properties": {"kind": {"const": "ready"}},
5918 "required": ["kind"]
5919 },
5920 "unevaluatedProperties": false
5921 }),
5922 json!({"kind": "ready"}),
5923 );
5924 }
5925
5926 #[test]
5927 fn admitted_if_and_then_union_successful_annotations() {
5928 assert_admitted_annotations_accept_only_evaluated_members(
5929 json!({
5930 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5931 "if": {
5932 "properties": {"kind": {"const": "ready"}},
5933 "required": ["kind"]
5934 },
5935 "then": {
5936 "properties": {"payload": {"type": "string"}},
5937 "required": ["payload"]
5938 },
5939 "unevaluatedProperties": false
5940 }),
5941 json!({"kind": "ready", "payload": "complete"}),
5942 );
5943 }
5944
5945 #[test]
5946 fn admitted_else_propagates_only_the_selected_branch_annotations() {
5947 assert_admitted_annotations_accept_only_evaluated_members(
5948 json!({
5949 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5950 "if": {
5951 "properties": {"kind": {"const": "primary"}},
5952 "required": ["kind"]
5953 },
5954 "else": {
5955 "properties": {
5956 "kind": {"const": "fallback"},
5957 "payload": {"type": "string"}
5958 },
5959 "required": ["kind", "payload"]
5960 },
5961 "unevaluatedProperties": false
5962 }),
5963 json!({"kind": "fallback", "payload": "complete"}),
5964 );
5965 }
5966
5967 #[test]
5968 fn admitted_nested_unevaluated_properties_annotations_reach_outer_schema() {
5969 assert_admitted_annotations_accept_only_evaluated_members(
5970 json!({
5971 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5972 "allOf": [{
5973 "properties": {"nested": {"type": "string"}},
5974 "required": ["nested"],
5975 "unevaluatedProperties": false
5976 }],
5977 "unevaluatedProperties": false
5978 }),
5979 json!({"nested": "ready"}),
5980 );
5981 }
5982
5983 fn pattern_property_work_schema() -> AdmittedSchema {
5984 let mut patterns = serde_json::Map::new();
5985 for index in 0..MAX_PATTERN_PROPERTIES {
5986 patterns.insert(format!("^never-{index}$"), Value::Bool(true));
5987 }
5988 admit_final_schema(json!({
5989 "$schema": FINAL_JSON_SCHEMA_DIALECT,
5990 "type": "object",
5991 "patternProperties": patterns
5992 }))
5993 .expect("the maximum bounded pattern family admits")
5994 }
5995
5996 fn object_with_null_members(count: usize) -> Value {
5997 let members: serde_json::Map<String, Value> = (0..count)
5998 .map(|index| (format!("field-{index}"), Value::Null))
5999 .collect();
6000 Value::Object(members)
6001 }
6002
6003 #[test]
6004 fn admitted_pattern_compilation_and_key_matching_share_work_limit() {
6005 let schema = pattern_property_work_schema();
6006 let accepted = object_with_null_members(62);
6007 schema
6008 .validate(&accepted)
6009 .expect("pattern compilation and 62 bounded key scans fit the work budget");
6010
6011 let planted = object_with_null_members(63);
6012 let errors = schema
6013 .validate(&planted)
6014 .expect_err("adding one key must exceed the shared regex work budget");
6015 assert!(
6016 errors
6017 .iter()
6018 .any(|error| error.message == "schema validation work limit exceeded")
6019 );
6020 assert_eq!(accepted.as_object().unwrap().len(), 62);
6021 }
6022
6023 #[test]
6024 fn raw_pattern_work_remains_legacy_while_admitted_final_is_bounded() {
6025 let mut patterns = serde_json::Map::new();
6026 for index in 0..MAX_PATTERN_PROPERTIES {
6027 patterns.insert(format!("^never-{index}$"), Value::Bool(true));
6028 }
6029 let schema = json!({
6030 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6031 "type": "object",
6032 "patternProperties": patterns,
6033 "additionalProperties": true
6034 });
6035 let instance = object_with_null_members(63);
6036
6037 validate(&schema, &instance)
6038 .expect("raw validation preserves its legacy pattern-work behavior");
6039 validate_strict(&schema, &instance)
6040 .expect("raw strict validation preserves its legacy pattern-work behavior");
6041
6042 let admitted = admit_final_schema(schema).expect("the bounded final schema admits");
6043 let errors = admitted
6044 .validate(&instance)
6045 .expect_err("admitted-final validation enforces the shared regex work budget");
6046 assert!(
6047 errors
6048 .iter()
6049 .any(|error| error.message == "schema validation work limit exceeded")
6050 );
6051 }
6052
6053 fn named_property_annotation_work_schema(count: usize) -> AdmittedSchema {
6054 let properties: serde_json::Map<String, Value> = (0..count)
6055 .map(|index| (format!("field-{index}"), Value::Bool(true)))
6056 .collect();
6057 admit_final_schema(json!({
6058 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6059 "type": "object",
6060 "properties": properties,
6061 "unevaluatedProperties": false
6062 }))
6063 .expect("the bounded named-property schema admits")
6064 }
6065
6066 #[test]
6067 fn admitted_key_annotation_bookkeeping_shares_work_limit() {
6068 let accepted_schema = named_property_annotation_work_schema(1_023);
6069 let accepted = object_with_null_members(1_023);
6070 accepted_schema.validate(&accepted).expect(
6071 "1,023 property validations, annotations, and preflight nodes fit the work budget",
6072 );
6073
6074 let planted_schema = named_property_annotation_work_schema(1_024);
6075 let planted = object_with_null_members(1_024);
6076 let errors = planted_schema
6077 .validate(&planted)
6078 .expect_err("adding one property must exceed the shared annotation work budget");
6079 assert!(
6080 errors
6081 .iter()
6082 .any(|error| error.message == "schema validation work limit exceeded")
6083 );
6084 assert_eq!(accepted.as_object().unwrap().len(), 1_023);
6085 }
6086
6087 fn repeated_pattern_work_schema(units: usize) -> AdmittedSchema {
6088 let unit = json!({
6089 "allOf": vec![
6090 json!({"$ref": "#/$defs/matching-pattern"});
6091 MAX_COMPOSITION_BRANCHES
6092 ]
6093 });
6094 admit_final_schema(json!({
6095 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6096 "$defs": {
6097 "matching-pattern": {"type": "string", "pattern": "^ready$"}
6098 },
6099 "allOf": vec![unit; units]
6100 }))
6101 .expect("the bounded repeated-pattern schema admits")
6102 }
6103
6104 #[test]
6105 fn admitted_string_pattern_compilation_and_matching_share_work_limit() {
6106 repeated_pattern_work_schema(1)
6107 .validate(&json!("ready"))
6108 .expect(
6109 "one repeated pattern unit and its local-reference traversal fit the work budget",
6110 );
6111
6112 let errors = repeated_pattern_work_schema(2)
6113 .validate(&json!("ready"))
6114 .expect_err("adding one repeated pattern unit must exceed the shared work budget");
6115 assert!(
6116 errors
6117 .iter()
6118 .any(|error| error.message == "schema validation work limit exceeded")
6119 );
6120 }
6121
6122 fn repeated_branch_probe_work_schema(units: usize) -> AdmittedSchema {
6123 let mut branches = vec![Value::Bool(false); MAX_COMPOSITION_BRANCHES];
6124 branches[0] = Value::Bool(true);
6125 let unit = json!({"oneOf": branches});
6126 admit_final_schema(json!({
6127 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6128 "allOf": vec![unit; units],
6129 "unevaluatedProperties": true
6130 }))
6131 .expect("the bounded repeated-branch schema admits")
6132 }
6133
6134 #[test]
6135 fn admitted_repeated_branch_probes_share_work_limit() {
6136 repeated_branch_probe_work_schema(10)
6137 .validate(&json!({}))
6138 .expect("ten repeated branch-probe units fit the work budget");
6139
6140 let errors = repeated_branch_probe_work_schema(11)
6141 .validate(&json!({}))
6142 .expect_err("adding one repeated branch unit must exceed the shared work budget");
6143 assert!(
6144 errors
6145 .iter()
6146 .any(|error| error.message == "schema validation work limit exceeded")
6147 );
6148 }
6149
6150 fn final_schema_format_annotation_schema() -> AdmittedSchema {
6151 admit_final_schema(json!({
6152 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6153 "type": "object",
6154 "properties": {
6155 "data": {"type": "string", "format": "byte"},
6156 "uri": {"type": "string", "format": "uri"},
6157 "template": {"type": "string", "format": "uri-template"}
6158 },
6159 "required": ["data", "uri", "template"],
6160 "additionalProperties": false
6161 }))
6162 .expect("the final format annotations admit")
6163 }
6164
6165 fn final_schema_format_instance() -> Value {
6166 json!({
6167 "data": "cGlubmVkIGZpbmFs",
6168 "uri": "https://example.test/resources?id=1#ready",
6169 "template": "mcp://resources/{id}{?cursor}"
6170 })
6171 }
6172
6173 #[test]
6174 fn final_schema_format_annotations_are_not_assertions_by_default() {
6175 final_schema_format_annotation_schema()
6176 .validate(&final_schema_format_instance())
6177 .expect("valid annotations do not reject an instance");
6178
6179 let mut planted = final_schema_format_instance();
6180 planted["uri"] = json!("not a uri");
6181 final_schema_format_annotation_schema()
6182 .validate(&planted)
6183 .expect("changing only an annotated URI value does not assert by default");
6184 }
6185
6186 #[test]
6187 fn format_assertion_meta_schema_is_refused_without_full_format_support() {
6188 let accepted = json!({
6189 "$id": "https://schemas.example.test/root.json",
6190 "$schema": "https://schemas.example.test/meta/format-assertion",
6191 "$defs": {
6192 "meta": {
6193 "$id": "meta/format-assertion",
6194 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6195 "$vocabulary": {
6196 CORE_VOCABULARY_URI: true,
6197 FORMAT_ANNOTATION_VOCABULARY_URI: true,
6198 FORMAT_ASSERTION_VOCABULARY_URI: false
6199 }
6200 }
6201 },
6202 "type": "string",
6203 "format": "uri"
6204 });
6205 let schema = admit_final_schema(accepted.clone())
6206 .expect("the annotation-only format vocabulary declaration admits");
6207 schema
6208 .validate(&json!("not a uri"))
6209 .expect("format annotations remain non-asserting");
6210
6211 let mut planted = accepted.clone();
6212 planted["$defs"]["meta"]["$vocabulary"][FORMAT_ASSERTION_VOCABULARY_URI] = json!(true);
6213 let error = admit_final_schema(planted)
6214 .expect_err("enabling only format assertion must fail closed without full support");
6215 assert_eq!(
6216 error.path(),
6217 "$.$vocabulary.https://json-schema.org/draft/2020-12/vocab/format-assertion"
6218 );
6219 assert_eq!(
6220 error.reason(),
6221 "format assertion vocabulary is not supported"
6222 );
6223 assert_eq!(
6224 accepted["$defs"]["meta"]["$vocabulary"][FORMAT_ASSERTION_VOCABULARY_URI],
6225 json!(false)
6226 );
6227 }
6228
6229 #[test]
6230 fn meta_schema_vocabulary_requires_the_core_vocabulary() {
6231 let accepted = json!({
6232 "$id": "https://schemas.example.test/root.json",
6233 "$schema": "https://schemas.example.test/meta/validation",
6234 "$defs": {
6235 "meta": {
6236 "$id": "meta/validation",
6237 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6238 "$vocabulary": {
6239 CORE_VOCABULARY_URI: true,
6240 VALIDATION_VOCABULARY_URI: true
6241 }
6242 }
6243 },
6244 "type": "string"
6245 });
6246 admit_final_schema(accepted.clone())
6247 .expect("a meta-schema that requires Core remains admissible");
6248
6249 let mut planted = accepted.clone();
6250 planted["$defs"]["meta"]["$vocabulary"] = json!({
6251 VALIDATION_VOCABULARY_URI: true
6252 });
6253 let error = admit_final_schema(planted)
6254 .expect_err("removing only the required Core vocabulary must reject");
6255 assert_eq!(error.path(), "$.$vocabulary");
6256 assert_eq!(
6257 error.reason(),
6258 "meta-schema $vocabulary must require the Draft 2020-12 core vocabulary"
6259 );
6260 assert_eq!(
6261 accepted["$defs"]["meta"]["$vocabulary"][CORE_VOCABULARY_URI],
6262 json!(true)
6263 );
6264 }
6265
6266 #[test]
6267 fn schema_admission_node_limit_positive_and_planted_negative() {
6268 let mut properties = serde_json::Map::new();
6269 for index in 0..(MAX_SCHEMA_ADMISSION_NODES - 1) {
6270 properties.insert(format!("field-{index}"), Value::Bool(true));
6271 }
6272 let accepted = json!({"type": "object", "properties": properties});
6273 admit_final_schema(accepted.clone())
6274 .expect("the exact schema-admission node budget is accepted");
6275
6276 let mut planted = accepted.clone();
6277 planted["properties"]
6278 .as_object_mut()
6279 .expect("schema properties stay an object")
6280 .insert(
6281 format!("field-{}", MAX_SCHEMA_ADMISSION_NODES - 1),
6282 Value::Bool(true),
6283 );
6284 let error = admit_final_schema(planted)
6285 .expect_err("adding one schema node beyond the admission budget must reject");
6286 assert_eq!(error.reason(), "schema admission node limit exceeded");
6287 assert_eq!(
6288 accepted["properties"].as_object().unwrap().len(),
6289 MAX_SCHEMA_ADMISSION_NODES - 1
6290 );
6291 }
6292
6293 #[test]
6294 fn instance_node_limit_positive_and_planted_negative() {
6295 let schema = admit_final_schema(Value::Bool(true)).expect("the true schema admits");
6296 let accepted = Value::Array(vec![Value::Null; MAX_SCHEMA_INSTANCE_NODES - 1]);
6297 let accepted_errors = schema
6298 .validate(&accepted)
6299 .expect_err("the exact node budget plus schema evaluation exceeds the work budget");
6300 assert!(
6301 accepted_errors
6302 .iter()
6303 .any(|error| { error.message == "schema validation work limit exceeded" })
6304 );
6305
6306 let mut planted = accepted.clone();
6307 planted.as_array_mut().unwrap().push(Value::Null);
6308 let errors = schema
6309 .validate(&planted)
6310 .expect_err("adding one instance node beyond the budget must reject");
6311 assert_eq!(
6312 errors[0].path,
6313 format!("root[{}]", MAX_SCHEMA_INSTANCE_NODES - 1)
6314 );
6315 assert_eq!(errors[0].message, "instance node limit exceeded");
6316 assert_eq!(
6317 accepted.as_array().unwrap().len(),
6318 MAX_SCHEMA_INSTANCE_NODES - 1
6319 );
6320 }
6321
6322 #[test]
6323 fn instance_preflight_work_is_accounted_with_a_one_node_negative() {
6324 let schema = admit_final_schema(Value::Bool(true)).expect("the true schema admits");
6325 let accepted = Value::Array(vec![Value::Null; MAX_SCHEMA_INSTANCE_NODES - 2]);
6326 schema
6327 .validate(&accepted)
6328 .expect("preflight plus one schema application fits the shared work budget");
6329
6330 let mut planted = accepted.clone();
6331 planted.as_array_mut().unwrap().push(Value::Null);
6332 let errors = schema
6333 .validate(&planted)
6334 .expect_err("adding one instance node exhausts the shared work budget");
6335 assert!(
6336 errors
6337 .iter()
6338 .any(|error| { error.message == "schema validation work limit exceeded" })
6339 );
6340 assert_eq!(
6341 accepted
6342 .as_array()
6343 .expect("the baseline remains an array")
6344 .len(),
6345 MAX_SCHEMA_INSTANCE_NODES - 2
6346 );
6347 }
6348
6349 #[test]
6350 fn raw_validation_does_not_charge_admitted_final_instance_preflight_work() {
6351 let legacy_instance = Value::Array(vec![Value::Null; MAX_SCHEMA_INSTANCE_NODES - 1]);
6352 validate(&Value::Bool(true), &legacy_instance)
6353 .expect("raw validation retains its legacy non-preflight work accounting");
6354 assert_eq!(
6355 legacy_instance
6356 .as_array()
6357 .expect("the legacy fixture remains an array")
6358 .len(),
6359 MAX_SCHEMA_INSTANCE_NODES - 1
6360 );
6361 }
6362
6363 fn composition_work_schema(branches: usize) -> Value {
6364 let unit = json!({"allOf": vec![Value::Bool(true); MAX_COMPOSITION_BRANCHES]});
6365 json!({"allOf": vec![unit; branches]})
6366 }
6367
6368 #[test]
6369 fn composition_work_limit_positive_and_planted_negative() {
6370 let accepted = composition_work_schema(MAX_COMPOSITION_BRANCHES - 1);
6371 validate(&accepted, &Value::Null)
6372 .expect("the exact shared composition-work budget is accepted");
6373
6374 let planted = composition_work_schema(MAX_COMPOSITION_BRANCHES);
6375 let errors = validate(&planted, &Value::Null)
6376 .expect_err("adding one composition branch beyond the shared work budget must reject");
6377 assert!(
6378 errors
6379 .iter()
6380 .any(|error| error.message == "schema validation work limit exceeded")
6381 );
6382 assert_eq!(
6383 accepted["allOf"].as_array().unwrap().len(),
6384 MAX_COMPOSITION_BRANCHES - 1
6385 );
6386 }
6387
6388 #[test]
6389 fn admitted_composition_branch_limit_positive_and_planted_negative() {
6390 let accepted = json!({"allOf": vec![Value::Bool(true); MAX_COMPOSITION_BRANCHES]});
6391 admit_final_schema(accepted.clone()).expect("the exact composition branch limit admits");
6392
6393 let mut planted = accepted.clone();
6394 planted["allOf"]
6395 .as_array_mut()
6396 .expect("the allOf fixture remains an array")
6397 .push(Value::Bool(true));
6398 let error = admit_final_schema(planted)
6399 .expect_err("adding one composition branch beyond the bound must reject admission");
6400 assert_eq!(error.path(), "$.allOf");
6401 assert_eq!(error.reason(), "composition keyword exceeds branch limit");
6402 assert_eq!(
6403 accepted["allOf"]
6404 .as_array()
6405 .expect("the baseline remains an array")
6406 .len(),
6407 MAX_COMPOSITION_BRANCHES
6408 );
6409 }
6410
6411 #[test]
6412 fn admitted_schema_rejects_unresolved_local_reference() {
6413 let error = admit_final_schema(json!({"$ref": "#/$defs/missing"}))
6414 .expect_err("unresolved local references fail before validation");
6415 assert_eq!(error.path(), "$.$ref");
6416 assert_eq!(error.reason(), "unresolved local schema reference");
6417 }
6418
6419 #[test]
6420 fn external_references_fail_closed_without_resolution() {
6421 let errors = validate(
6422 &json!({"$ref": "https://schemas.example.test/tool.json"}),
6423 &json!({"input": "value"}),
6424 )
6425 .expect_err("external references must not acquire network or filesystem authority");
6426
6427 assert_eq!(errors.len(), 1);
6428 assert_eq!(errors[0].path, "root");
6429 assert_eq!(
6430 errors[0].message,
6431 "external schema reference is not allowed"
6432 );
6433 }
6434
6435 #[test]
6436 fn admitted_local_reference_target_membership_positive_and_planted_negative() {
6437 let accepted = json!({
6438 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6439 "$defs": {"always": true},
6440 "default": {"annotation": "object"},
6441 "$ref": "#/$defs/always"
6442 });
6443 let schema = admit_final_schema(accepted.clone())
6444 .expect("a local reference to a boolean schema admits");
6445 let instance = json!({"any": "value"});
6446
6447 schema
6448 .validate(&instance)
6449 .expect("the referenced true schema accepts the instance");
6450
6451 let mut planted = accepted.clone();
6452 planted["$ref"] = json!("#/default");
6453 let error = admit_final_schema(planted)
6454 .expect_err("changing only the target to a non-schema annotation must reject");
6455 assert_eq!(error.path(), "$.$ref");
6456 assert_eq!(
6457 error.reason(),
6458 "local schema reference target is not an admitted schema node"
6459 );
6460 assert_eq!(accepted["$ref"], json!("#/$defs/always"));
6461 assert_eq!(instance, json!({"any": "value"}));
6462 }
6463
6464 #[test]
6465 fn admitted_dynamic_reference_target_membership_positive_and_planted_negative() {
6466 let accepted = json!({
6467 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6468 "$defs": {"always": true},
6469 "default": {"annotation": "object"},
6470 "$dynamicRef": "#/$defs/always"
6471 });
6472 let schema = admit_final_schema(accepted.clone())
6473 .expect("a local dynamic reference to a boolean schema admits");
6474 let instance = json!({"any": "value"});
6475
6476 schema
6477 .validate(&instance)
6478 .expect("the dynamically referenced true schema accepts the instance");
6479
6480 let mut planted = accepted.clone();
6481 planted["$dynamicRef"] = json!("#/default");
6482 let error = admit_final_schema(planted)
6483 .expect_err("changing only the dynamic target to a non-schema annotation must reject");
6484 assert_eq!(error.path(), "$.$dynamicRef");
6485 assert_eq!(
6486 error.reason(),
6487 "local schema reference target is not an admitted schema node"
6488 );
6489 assert_eq!(accepted["$dynamicRef"], json!("#/$defs/always"));
6490 assert_eq!(instance, json!({"any": "value"}));
6491 }
6492
6493 #[test]
6494 fn admitted_enum_entry_limit_positive_and_planted_negative() {
6495 let values: Vec<Value> = (0..MAX_SCHEMA_ASSERTION_ENTRIES)
6496 .map(|index| json!(format!("value-{index}")))
6497 .collect();
6498 let accepted = json!({
6499 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6500 "enum": values
6501 });
6502 let schema =
6503 admit_final_schema(accepted.clone()).expect("the exact enum entry budget admits");
6504 let instance = json!("value-0");
6505
6506 schema
6507 .validate(&instance)
6508 .expect("the first bounded enum value validates");
6509
6510 let mut planted = accepted.clone();
6511 planted["enum"]
6512 .as_array_mut()
6513 .expect("enum remains an array")
6514 .push(json!("value-over-limit"));
6515 let error = admit_final_schema(planted)
6516 .expect_err("adding one enum member beyond the limit must reject");
6517 assert_eq!(error.path(), "$.enum");
6518 assert_eq!(error.reason(), "enum exceeds entry limit");
6519 assert_eq!(
6520 accepted["enum"]
6521 .as_array()
6522 .expect("enum remains an array")
6523 .len(),
6524 MAX_SCHEMA_ASSERTION_ENTRIES
6525 );
6526 assert_eq!(instance, json!("value-0"));
6527 }
6528
6529 #[test]
6530 fn admitted_required_entry_limit_positive_and_planted_negative() {
6531 let members: Vec<Value> = (0..MAX_SCHEMA_ASSERTION_ENTRIES)
6532 .map(|index| json!(format!("field-{index}")))
6533 .collect();
6534 let accepted = json!({
6535 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6536 "type": "object",
6537 "required": members
6538 });
6539 let schema =
6540 admit_final_schema(accepted.clone()).expect("the exact required-entry budget admits");
6541 let instance = object_with_null_members(MAX_SCHEMA_ASSERTION_ENTRIES);
6542
6543 schema
6544 .validate(&instance)
6545 .expect("every required property at the entry limit is present");
6546
6547 let mut planted = accepted.clone();
6548 planted["required"]
6549 .as_array_mut()
6550 .expect("required remains an array")
6551 .push(json!("field-over-limit"));
6552 let error = admit_final_schema(planted)
6553 .expect_err("adding one required member beyond the limit must reject");
6554 assert_eq!(error.path(), "$.required");
6555 assert_eq!(error.reason(), "required exceeds entry limit");
6556 assert_eq!(
6557 accepted["required"]
6558 .as_array()
6559 .expect("required remains an array")
6560 .len(),
6561 MAX_SCHEMA_ASSERTION_ENTRIES
6562 );
6563 assert_eq!(
6564 instance
6565 .as_object()
6566 .expect("instance remains an object")
6567 .len(),
6568 MAX_SCHEMA_ASSERTION_ENTRIES
6569 );
6570 }
6571
6572 #[test]
6573 fn admitted_dependent_required_entry_limit_positive_and_planted_negative() {
6574 let members: Vec<Value> = (0..MAX_SCHEMA_ASSERTION_ENTRIES)
6575 .map(|index| json!(format!("field-{index}")))
6576 .collect();
6577 let accepted = json!({
6578 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6579 "type": "object",
6580 "dependentRequired": {"trigger": members}
6581 });
6582 let schema = admit_final_schema(accepted.clone())
6583 .expect("the exact dependent-required entry budget admits");
6584 let mut instance = object_with_null_members(MAX_SCHEMA_ASSERTION_ENTRIES);
6585 instance["trigger"] = Value::Null;
6586
6587 schema
6588 .validate(&instance)
6589 .expect("every bounded dependent requirement is present");
6590
6591 let mut planted = accepted.clone();
6592 planted["dependentRequired"]["trigger"]
6593 .as_array_mut()
6594 .expect("dependent-required members remain an array")
6595 .push(json!("field-over-limit"));
6596 let error = admit_final_schema(planted)
6597 .expect_err("adding one dependent requirement beyond the limit must reject");
6598 assert_eq!(error.path(), "$.dependentRequired");
6599 assert_eq!(
6600 error.reason(),
6601 "dependentRequired values exceed entry limit"
6602 );
6603 assert_eq!(
6604 accepted["dependentRequired"]["trigger"]
6605 .as_array()
6606 .expect("baseline members remain an array")
6607 .len(),
6608 MAX_SCHEMA_ASSERTION_ENTRIES
6609 );
6610 assert_eq!(
6611 instance
6612 .as_object()
6613 .expect("instance remains an object")
6614 .len(),
6615 MAX_SCHEMA_ASSERTION_ENTRIES + 1
6616 );
6617 }
6618
6619 #[test]
6620 fn admitted_dependent_required_work_budget_includes_root_overhead() {
6621 let mut dependencies = serde_json::Map::new();
6622 let mut instance = serde_json::Map::new();
6623 for index in 0..MAX_SCHEMA_ASSERTION_ENTRIES {
6624 let member_count = if index + 1 == MAX_SCHEMA_ASSERTION_ENTRIES {
6625 MAX_SCHEMA_ASSERTION_ENTRIES - 2
6626 } else {
6627 MAX_SCHEMA_ASSERTION_ENTRIES - 1
6628 };
6629 let trigger = format!("trigger-{index}");
6630 let mut members = Vec::with_capacity(member_count);
6631 for member_index in 0..member_count {
6632 let member = format!("required-{index}-{member_index}");
6633 members.push(json!(member.clone()));
6634 if index == 0 {
6635 instance.insert(member, Value::Null);
6636 }
6637 }
6638 dependencies.insert(trigger.clone(), Value::Array(members));
6639 if index == 0 {
6640 instance.insert(trigger, Value::Null);
6641 }
6642 }
6643 let accepted = json!({
6644 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6645 "type": "object",
6646 "dependentRequired": dependencies
6647 });
6648 let instance = Value::Object(instance);
6649
6650 admit_final_schema(accepted.clone())
6651 .expect("the exact dependentRequired payload plus root work budget admits")
6652 .validate(&instance)
6653 .expect("the exact dependentRequired work budget validates");
6654
6655 let mut planted = accepted.clone();
6656 planted["dependentRequired"]["trigger-0"]
6657 .as_array_mut()
6658 .expect("planted dependent requirements remain an array")
6659 .push(json!("required-over-budget"));
6660 let error = admit_final_schema(planted)
6661 .expect_err("one extra dependent requirement beyond root-inclusive work must reject");
6662 assert_eq!(error.path(), "$.dependentRequired");
6663 assert_eq!(
6664 error.reason(),
6665 "dependentRequired exceeds validation work budget"
6666 );
6667 assert_eq!(
6668 accepted["dependentRequired"]
6669 .as_object()
6670 .expect("baseline dependencies remain an object")
6671 .len(),
6672 MAX_SCHEMA_ASSERTION_ENTRIES
6673 );
6674 assert_eq!(
6675 accepted["dependentRequired"]["trigger-0"]
6676 .as_array()
6677 .expect("baseline dependency members remain an array")
6678 .len(),
6679 MAX_SCHEMA_ASSERTION_ENTRIES - 1
6680 );
6681 assert_eq!(
6682 instance
6683 .as_object()
6684 .expect("instance remains an object")
6685 .len(),
6686 MAX_SCHEMA_ASSERTION_ENTRIES
6687 );
6688 }
6689
6690 #[test]
6691 fn admitted_pattern_byte_limit_positive_and_planted_negative() {
6692 let pattern = "a".repeat(MAX_PATTERN_BYTES);
6693 let accepted = json!({
6694 "$schema": FINAL_JSON_SCHEMA_DIALECT,
6695 "type": "string",
6696 "pattern": pattern
6697 });
6698 let schema =
6699 admit_final_schema(accepted.clone()).expect("the exact pattern byte budget admits");
6700 let instance = json!("a".repeat(MAX_PATTERN_BYTES));
6701
6702 schema
6703 .validate(&instance)
6704 .expect("the bounded exact-length pattern matches the instance");
6705
6706 let mut planted = accepted.clone();
6707 planted["pattern"] = json!(format!("{}a", accepted["pattern"].as_str().unwrap()));
6708 let error = admit_final_schema(planted)
6709 .expect_err("adding one pattern byte beyond the limit must reject");
6710 assert_eq!(error.path(), "$.pattern");
6711 assert_eq!(error.reason(), "pattern exceeds byte limit");
6712 assert_eq!(
6713 accepted["pattern"]
6714 .as_str()
6715 .expect("baseline pattern remains a string")
6716 .len(),
6717 MAX_PATTERN_BYTES
6718 );
6719 assert_eq!(instance, json!("a".repeat(MAX_PATTERN_BYTES)));
6720 }
6721
6722 #[test]
6723 fn test_type_validation_string() {
6724 let schema = json!({"type": "string"});
6725 assert!(validate(&schema, &json!("hello")).is_ok());
6726 assert!(validate(&schema, &json!(123)).is_err());
6727 }
6728
6729 #[test]
6730 fn test_type_validation_number() {
6731 let schema = json!({"type": "number"});
6732 assert!(validate(&schema, &json!(123)).is_ok());
6733 assert!(validate(&schema, &json!(12.5)).is_ok());
6734 assert!(validate(&schema, &json!("hello")).is_err());
6735 }
6736
6737 #[test]
6738 fn test_type_validation_integer() {
6739 let schema = json!({"type": "integer"});
6740 assert!(validate(&schema, &json!(123)).is_ok());
6741 assert!(validate(&schema, &json!(12.5)).is_err());
6742 }
6743
6744 #[test]
6745 fn test_type_validation_boolean() {
6746 let schema = json!({"type": "boolean"});
6747 assert!(validate(&schema, &json!(true)).is_ok());
6748 assert!(validate(&schema, &json!(false)).is_ok());
6749 assert!(validate(&schema, &json!(1)).is_err());
6750 }
6751
6752 #[test]
6753 fn test_type_validation_object() {
6754 let schema = json!({"type": "object"});
6755 assert!(validate(&schema, &json!({})).is_ok());
6756 assert!(validate(&schema, &json!({"a": 1})).is_ok());
6757 assert!(validate(&schema, &json!([])).is_err());
6758 }
6759
6760 #[test]
6761 fn test_type_validation_array() {
6762 let schema = json!({"type": "array"});
6763 assert!(validate(&schema, &json!([])).is_ok());
6764 assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
6765 assert!(validate(&schema, &json!({})).is_err());
6766 }
6767
6768 #[test]
6769 fn test_type_validation_null() {
6770 let schema = json!({"type": "null"});
6771 assert!(validate(&schema, &json!(null)).is_ok());
6772 assert!(validate(&schema, &json!(0)).is_err());
6773 }
6774
6775 #[test]
6776 fn test_type_validation_union() {
6777 let schema = json!({"type": ["string", "number"]});
6778 assert!(validate(&schema, &json!("hello")).is_ok());
6779 assert!(validate(&schema, &json!(123)).is_ok());
6780 assert!(validate(&schema, &json!(true)).is_err());
6781 }
6782
6783 #[test]
6784 fn test_required_fields() {
6785 let schema = json!({
6786 "type": "object",
6787 "properties": {
6788 "name": {"type": "string"},
6789 "age": {"type": "integer"}
6790 },
6791 "required": ["name"]
6792 });
6793
6794 assert!(validate(&schema, &json!({"name": "Alice"})).is_ok());
6795 assert!(validate(&schema, &json!({"name": "Alice", "age": 30})).is_ok());
6796 assert!(validate(&schema, &json!({"age": 30})).is_err());
6797 assert!(validate(&schema, &json!({})).is_err());
6798 }
6799
6800 #[test]
6801 fn test_enum_validation() {
6802 let schema = json!({"enum": ["red", "green", "blue"]});
6803 assert!(validate(&schema, &json!("red")).is_ok());
6804 assert!(validate(&schema, &json!("yellow")).is_err());
6805 }
6806
6807 #[test]
6808 fn test_const_validation() {
6809 let schema = json!({"const": "fixed"});
6810 assert!(validate(&schema, &json!("fixed")).is_ok());
6811 assert!(validate(&schema, &json!("other")).is_err());
6812 }
6813
6814 #[test]
6815 fn test_string_length() {
6816 let schema = json!({
6817 "type": "string",
6818 "minLength": 2,
6819 "maxLength": 5
6820 });
6821
6822 assert!(validate(&schema, &json!("ab")).is_ok());
6823 assert!(validate(&schema, &json!("abcde")).is_ok());
6824 assert!(validate(&schema, &json!("a")).is_err());
6825 assert!(validate(&schema, &json!("abcdef")).is_err());
6826 }
6827
6828 #[test]
6829 fn test_string_pattern() {
6830 let schema = json!({
6831 "type": "string",
6832 "pattern": "^[a-z]+$"
6833 });
6834
6835 assert!(validate(&schema, &json!("hello")).is_ok());
6836 assert!(validate(&schema, &json!("Hello")).is_err());
6837 assert!(validate(&schema, &json!("hello123")).is_err());
6838 }
6839
6840 #[test]
6841 fn test_string_pattern_invalid_regex_is_error() {
6842 let schema = json!({
6843 "type": "string",
6844 "pattern": "("
6845 });
6846
6847 assert!(validate(&schema, &json!("anything")).is_err());
6848 }
6849
6850 #[test]
6851 fn test_number_range() {
6852 let schema = json!({
6853 "type": "number",
6854 "minimum": 0,
6855 "maximum": 100
6856 });
6857
6858 assert!(validate(&schema, &json!(0)).is_ok());
6859 assert!(validate(&schema, &json!(50)).is_ok());
6860 assert!(validate(&schema, &json!(100)).is_ok());
6861 assert!(validate(&schema, &json!(-1)).is_err());
6862 assert!(validate(&schema, &json!(101)).is_err());
6863 }
6864
6865 #[test]
6866 fn test_number_exclusive_range() {
6867 let schema = json!({
6868 "type": "number",
6869 "exclusiveMinimum": 0,
6870 "exclusiveMaximum": 10
6871 });
6872
6873 assert!(validate(&schema, &json!(1)).is_ok());
6874 assert!(validate(&schema, &json!(9)).is_ok());
6875 assert!(validate(&schema, &json!(0)).is_err());
6876 assert!(validate(&schema, &json!(10)).is_err());
6877 }
6878
6879 #[test]
6880 fn test_array_items() {
6881 let schema = json!({
6882 "type": "array",
6883 "items": {"type": "integer"}
6884 });
6885
6886 assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
6887 assert!(validate(&schema, &json!([])).is_ok());
6888 assert!(validate(&schema, &json!([1, "two", 3])).is_err());
6889 }
6890
6891 #[test]
6892 fn test_array_length() {
6893 let schema = json!({
6894 "type": "array",
6895 "minItems": 1,
6896 "maxItems": 3
6897 });
6898
6899 assert!(validate(&schema, &json!([1])).is_ok());
6900 assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
6901 assert!(validate(&schema, &json!([])).is_err());
6902 assert!(validate(&schema, &json!([1, 2, 3, 4])).is_err());
6903 }
6904
6905 #[test]
6906 fn test_unique_items() {
6907 let schema = json!({
6908 "type": "array",
6909 "uniqueItems": true
6910 });
6911
6912 assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
6913 assert!(validate(&schema, &json!([1, 1, 2])).is_err());
6914 }
6915
6916 #[test]
6917 fn test_nested_object() {
6918 let schema = json!({
6919 "type": "object",
6920 "properties": {
6921 "person": {
6922 "type": "object",
6923 "properties": {
6924 "name": {"type": "string"},
6925 "age": {"type": "integer"}
6926 },
6927 "required": ["name"]
6928 }
6929 }
6930 });
6931
6932 assert!(validate(&schema, &json!({"person": {"name": "Alice"}})).is_ok());
6933 assert!(validate(&schema, &json!({"person": {"name": "Alice", "age": 30}})).is_ok());
6934 assert!(validate(&schema, &json!({"person": {"age": 30}})).is_err());
6935 }
6936
6937 #[test]
6938 fn test_additional_properties_false() {
6939 let schema = json!({
6940 "type": "object",
6941 "properties": {
6942 "name": {"type": "string"}
6943 },
6944 "additionalProperties": false
6945 });
6946
6947 assert!(validate(&schema, &json!({"name": "Alice"})).is_ok());
6948 assert!(validate(&schema, &json!({})).is_ok());
6949 assert!(validate(&schema, &json!({"name": "Alice", "extra": 1})).is_err());
6950 }
6951
6952 #[test]
6953 fn test_boolean_schema() {
6954 assert!(validate(&json!(true), &json!("anything")).is_ok());
6956 assert!(validate(&json!(true), &json!(123)).is_ok());
6957
6958 assert!(validate(&json!(false), &json!("anything")).is_err());
6960 }
6961
6962 #[test]
6963 fn test_multiple_errors() {
6964 let schema = json!({
6965 "type": "object",
6966 "properties": {
6967 "name": {"type": "string"},
6968 "age": {"type": "integer"}
6969 },
6970 "required": ["name", "age"]
6971 });
6972
6973 let result = validate(&schema, &json!({}));
6974 assert!(result.is_err());
6975 let errors = result.unwrap_err();
6976 assert_eq!(errors.len(), 2); }
6978
6979 #[test]
6980 fn test_error_path() {
6981 let schema = json!({
6982 "type": "object",
6983 "properties": {
6984 "items": {
6985 "type": "array",
6986 "items": {"type": "integer"}
6987 }
6988 }
6989 });
6990
6991 let result = validate(&schema, &json!({"items": [1, "two", 3]}));
6992 assert!(result.is_err());
6993 let errors = result.unwrap_err();
6994 assert_eq!(errors.len(), 1);
6995 assert_eq!(errors[0].path, "root.items[1]");
6996 }
6997
6998 #[test]
7003 fn test_validate_strict_rejects_extra_properties() {
7004 let schema = json!({
7005 "type": "object",
7006 "properties": {
7007 "name": {"type": "string"}
7008 }
7009 });
7010
7011 assert!(validate(&schema, &json!({"name": "Alice", "extra": 123})).is_ok());
7013
7014 assert!(validate_strict(&schema, &json!({"name": "Alice", "extra": 123})).is_err());
7016
7017 assert!(validate_strict(&schema, &json!({"name": "Alice"})).is_ok());
7019 }
7020
7021 #[test]
7022 fn test_validate_strict_nested_objects() {
7023 let schema = json!({
7024 "type": "object",
7025 "properties": {
7026 "person": {
7027 "type": "object",
7028 "properties": {
7029 "name": {"type": "string"}
7030 }
7031 }
7032 }
7033 });
7034
7035 assert!(
7037 validate(
7038 &schema,
7039 &json!({
7040 "person": {"name": "Alice", "age": 30}
7041 })
7042 )
7043 .is_ok()
7044 );
7045
7046 assert!(
7048 validate_strict(
7049 &schema,
7050 &json!({
7051 "person": {"name": "Alice", "age": 30}
7052 })
7053 )
7054 .is_err()
7055 );
7056
7057 assert!(
7059 validate_strict(
7060 &schema,
7061 &json!({
7062 "person": {"name": "Alice"}
7063 })
7064 )
7065 .is_ok()
7066 );
7067 }
7068
7069 #[test]
7070 fn test_validate_strict_preserves_explicit_additional_properties() {
7071 let schema = json!({
7073 "type": "object",
7074 "properties": {
7075 "name": {"type": "string"}
7076 },
7077 "additionalProperties": {"type": "integer"}
7078 });
7079
7080 assert!(
7082 validate_strict(
7083 &schema,
7084 &json!({
7085 "name": "Alice",
7086 "count": 42
7087 })
7088 )
7089 .is_ok()
7090 );
7091
7092 assert!(
7094 validate_strict(
7095 &schema,
7096 &json!({
7097 "name": "Alice",
7098 "count": "not an integer"
7099 })
7100 )
7101 .is_err()
7102 );
7103 }
7104
7105 #[test]
7106 fn test_validate_strict_array_items() {
7107 let schema = json!({
7108 "type": "array",
7109 "items": {
7110 "type": "object",
7111 "properties": {
7112 "id": {"type": "integer"}
7113 }
7114 }
7115 });
7116
7117 assert!(
7119 validate(
7120 &schema,
7121 &json!([
7122 {"id": 1, "extra": "value"}
7123 ])
7124 )
7125 .is_ok()
7126 );
7127
7128 assert!(
7130 validate_strict(
7131 &schema,
7132 &json!([
7133 {"id": 1, "extra": "value"}
7134 ])
7135 )
7136 .is_err()
7137 );
7138
7139 assert!(
7141 validate_strict(
7142 &schema,
7143 &json!([
7144 {"id": 1}
7145 ])
7146 )
7147 .is_ok()
7148 );
7149 }
7150
7151 #[test]
7152 fn test_validate_strict_empty_schema() {
7153 let schema = json!({});
7155
7156 assert!(validate_strict(&schema, &json!({"anything": "goes"})).is_ok());
7158 }
7159
7160 #[test]
7161 fn test_validate_strict_non_object_types() {
7162 let string_schema = json!({"type": "string"});
7164 assert!(validate_strict(&string_schema, &json!("hello")).is_ok());
7165
7166 let number_schema = json!({"type": "number"});
7167 assert!(validate_strict(&number_schema, &json!(42)).is_ok());
7168
7169 let array_schema = json!({"type": "array"});
7170 assert!(validate_strict(&array_schema, &json!([1, 2, 3])).is_ok());
7171 }
7172
7173 #[test]
7178 fn validation_error_display_and_error_trait() {
7179 let err = ValidationError {
7180 path: "root.name".to_string(),
7181 message: "expected string".to_string(),
7182 };
7183 assert_eq!(err.to_string(), "root.name: expected string");
7184 let _: &dyn std::error::Error = &err;
7185 }
7186
7187 #[test]
7188 fn validation_error_debug_and_clone() {
7189 let err = ValidationError {
7190 path: "root".to_string(),
7191 message: "missing".to_string(),
7192 };
7193 let debug = format!("{err:?}");
7194 assert!(debug.contains("ValidationError"));
7195
7196 let cloned = err.clone();
7197 assert_eq!(cloned.path, "root");
7198 assert_eq!(cloned.message, "missing");
7199 }
7200
7201 #[test]
7202 fn json_type_name_all_types() {
7203 assert_eq!(json_type_name(&json!(null)), "null");
7204 assert_eq!(json_type_name(&json!(true)), "boolean");
7205 assert_eq!(json_type_name(&json!(42)), "integer");
7206 assert_eq!(json_type_name(&json!(3.14)), "number");
7207 assert_eq!(json_type_name(&json!("hello")), "string");
7208 assert_eq!(json_type_name(&json!([])), "array");
7209 assert_eq!(json_type_name(&json!({})), "object");
7210 }
7211
7212 #[test]
7213 fn number_multiple_of() {
7214 let schema = json!({"type": "number", "multipleOf": 3});
7215 assert!(validate(&schema, &json!(9)).is_ok());
7216 assert!(validate(&schema, &json!(6)).is_ok());
7217 assert!(validate(&schema, &json!(0)).is_ok());
7218 assert!(validate(&schema, &json!(7)).is_err());
7219 }
7220
7221 #[test]
7222 fn object_min_max_properties() {
7223 let schema = json!({
7224 "type": "object",
7225 "minProperties": 1,
7226 "maxProperties": 2
7227 });
7228
7229 assert!(validate(&schema, &json!({"a": 1})).is_ok());
7230 assert!(validate(&schema, &json!({"a": 1, "b": 2})).is_ok());
7231 assert!(validate(&schema, &json!({})).is_err());
7232 assert!(validate(&schema, &json!({"a": 1, "b": 2, "c": 3})).is_err());
7233 }
7234
7235 #[test]
7236 fn prefix_items_tuple_validation() {
7237 let schema = json!({
7238 "type": "array",
7239 "prefixItems": [
7240 {"type": "string"},
7241 {"type": "integer"}
7242 ]
7243 });
7244
7245 assert!(validate(&schema, &json!(["hello", 42])).is_ok());
7246 assert!(validate(&schema, &json!(["hello", 42, true])).is_ok()); assert!(validate(&schema, &json!([123, "wrong"])).is_err()); }
7249
7250 #[test]
7251 fn prefix_items_with_additional_items_schema() {
7252 let schema = json!({
7253 "type": "array",
7254 "prefixItems": [
7255 {"type": "string"}
7256 ],
7257 "items": {"type": "integer"}
7258 });
7259
7260 assert!(validate(&schema, &json!(["hello", 1, 2])).is_ok());
7262 assert!(validate(&schema, &json!(["hello", "bad"])).is_err());
7263 }
7264
7265 #[test]
7266 fn items_as_array_draft4_fallback() {
7267 let schema = json!({
7269 "type": "array",
7270 "items": [
7271 {"type": "string"},
7272 {"type": "integer"}
7273 ]
7274 });
7275
7276 assert!(validate(&schema, &json!(["hello", 42])).is_ok());
7277 assert!(validate(&schema, &json!([123, "wrong"])).is_err());
7278 }
7279
7280 #[test]
7281 fn additional_properties_as_schema() {
7282 let schema = json!({
7283 "type": "object",
7284 "properties": {
7285 "name": {"type": "string"}
7286 },
7287 "additionalProperties": {"type": "integer"}
7288 });
7289
7290 assert!(validate(&schema, &json!({"name": "Alice", "count": 42})).is_ok());
7291 assert!(validate(&schema, &json!({"name": "Alice", "bad": "string"})).is_err());
7292 }
7293
7294 #[test]
7295 fn strict_schema_with_prefix_items() {
7296 let schema = json!({
7297 "type": "array",
7298 "prefixItems": [
7299 {
7300 "type": "object",
7301 "properties": {
7302 "id": {"type": "integer"}
7303 }
7304 }
7305 ]
7306 });
7307
7308 assert!(validate_strict(&schema, &json!([{"id": 1}])).is_ok());
7310 assert!(validate_strict(&schema, &json!([{"id": 1, "extra": "val"}])).is_err());
7311 }
7312
7313 #[test]
7314 fn strict_schema_with_union_type() {
7315 let schema = json!({
7316 "type": ["object", "null"],
7317 "properties": {
7318 "name": {"type": "string"}
7319 }
7320 });
7321
7322 assert!(validate_strict(&schema, &json!(null)).is_ok());
7324 assert!(validate_strict(&schema, &json!({"name": "Alice"})).is_ok());
7325 assert!(validate_strict(&schema, &json!({"name": "Alice", "extra": 1})).is_err());
7326 }
7327
7328 #[test]
7329 fn unknown_type_in_matches_type_is_permissive() {
7330 let schema = json!({"type": "custom_extension"});
7331 assert!(validate(&schema, &json!("anything")).is_ok());
7333 assert!(validate(&schema, &json!(42)).is_ok());
7334 }
7335
7336 #[test]
7337 fn invalid_schema_not_an_object() {
7338 assert!(validate(&json!(42), &json!("anything")).is_ok());
7340 assert!(validate(&json!("bad_schema"), &json!(123)).is_ok());
7341 }
7342}