Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 74 additions & 3 deletions crates/jsonschema/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
},
node::{PendingSchemaNode, SchemaNode},
options::{PatternEngineOptions, ValidationOptions},
paths::{Location, LocationSegment},
paths::{write_escaped_str, Location, LocationSegment},
types::{JsonType, JsonTypeSet},
validator::Validate,
ValidationError, Validator,
Expand Down Expand Up @@ -729,7 +729,22 @@ pub(crate) fn build_validator(
// Finally, compile the validator
let root = compile(&ctx, resource_ref).map_err(ValidationError::to_owned)?;
let draft = config.draft();
Ok(Validator { root, draft })

// Collect all subschemas with their JSON pointers
if config.collect_pointer_subschemas {
let subschemas = Some(collect_subschemas(&ctx, schema));
return Ok(Validator {
root,
draft,
subschemas,
});
}

Ok(Validator {
root,
draft,
subschemas: None,
})
}

#[cfg(feature = "resolve-async")]
Expand Down Expand Up @@ -801,7 +816,22 @@ pub(crate) async fn build_validator_async(

let root = compile(&ctx, resource_ref).map_err(ValidationError::to_owned)?;
let draft = config.draft();
Ok(Validator { root, draft })

// Collect all subschemas with their JSON pointers
if config.collect_pointer_subschemas {
let subschemas = Some(collect_subschemas(&ctx, schema));
return Ok(Validator {
root,
draft,
subschemas,
});
}

Ok(Validator {
root,
draft,
subschemas: None,
})
}

fn annotations_to_value(annotations: AHashMap<String, Value>) -> Arc<Value> {
Expand All @@ -824,6 +854,47 @@ fn collect_resource_pairs(
)
}

/// Iteratively collect all subschemas from a schema document with their JSON pointers
fn collect_subschemas(ctx: &Context, schema: &Value) -> AHashMap<String, SchemaNode> {
let mut subschemas = AHashMap::new();

// Stack to hold (schema, pointer) pairs to process
let mut stack = vec![(schema, "#".to_string())];

while let Some((current_schema, pointer)) = stack.pop() {
// Compile and store the current schema if it's not the root (empty pointer)
let resource_ref = ctx.as_resource_ref(current_schema);
let ctx_at_location = ctx.new_at_location(pointer.as_str());
if let Ok(node) = compile(&ctx_at_location, resource_ref) {
subschemas.insert(pointer.clone(), node);
} else {
// Skip schemas that fail to compile (e.g., invalid references)
// This allows the validator to still work with valid subschemas
}

// Process subschemas based on schema structure
match current_schema {
Value::Object(obj) => {
stack.extend(obj.iter().map(|(key, value)| {
let mut buffer = String::new();
write_escaped_str(&mut buffer, key);
(value, format!("{pointer}/{buffer}"))
}));
}
Value::Array(arr) => {
stack.extend(
arr.iter()
.enumerate()
.map(|(idx, item)| (item, format!("{pointer}/{idx}"))),
);
}
_ => {}
}
}

subschemas
}

fn validate_schema(draft: Draft, schema: &Value) -> Result<(), ValidationError<'static>> {
// Boolean schemas are always valid per the spec, skip validation
if schema.is_boolean() {
Expand Down
11 changes: 11 additions & 0 deletions crates/jsonschema/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub struct ValidationOptions<R = Arc<dyn Retrieve>> {
formats: AHashMap<String, Arc<dyn Format>>,
validate_formats: Option<bool>,
pub(crate) validate_schema: bool,
pub(crate) collect_pointer_subschemas: bool,
ignore_unknown_formats: bool,
keywords: AHashMap<String, Arc<dyn KeywordFactory>>,
pattern_options: PatternEngineOptions,
Expand All @@ -51,6 +52,7 @@ impl Default for ValidationOptions<Arc<dyn Retrieve>> {
formats: AHashMap::default(),
validate_formats: None,
validate_schema: true,
collect_pointer_subschemas: false,
ignore_unknown_formats: true,
keywords: AHashMap::default(),
pattern_options: PatternEngineOptions::default(),
Expand All @@ -73,6 +75,7 @@ impl Default for ValidationOptions<Arc<dyn referencing::AsyncRetrieve>> {
formats: AHashMap::default(),
validate_formats: None,
validate_schema: true,
collect_pointer_subschemas: false,
ignore_unknown_formats: true,
keywords: AHashMap::default(),
pattern_options: PatternEngineOptions::default(),
Expand Down Expand Up @@ -413,6 +416,12 @@ impl<R> ValidationOptions<R> {
self.validate_schema = false;
self
}
/// Collect JSON pointer subschemas during compilation.
#[must_use]
pub fn collect_pointer_subschemas(mut self) -> Self {
self.collect_pointer_subschemas = true;
self
}
/// Set whether to validate formats.
///
/// Default behavior depends on the draft version. This method overrides
Expand Down Expand Up @@ -748,6 +757,7 @@ impl ValidationOptions<Arc<dyn referencing::AsyncRetrieve>> {
formats: self.formats,
validate_formats: self.validate_formats,
validate_schema: self.validate_schema,
collect_pointer_subschemas: self.collect_pointer_subschemas,
ignore_unknown_formats: self.ignore_unknown_formats,
keywords: self.keywords,
pattern_options: self.pattern_options,
Expand Down Expand Up @@ -783,6 +793,7 @@ impl ValidationOptions<Arc<dyn referencing::AsyncRetrieve>> {
formats: self.formats,
validate_formats: self.validate_formats,
validate_schema: self.validate_schema,
collect_pointer_subschemas: self.collect_pointer_subschemas,
ignore_unknown_formats: self.ignore_unknown_formats,
keywords: self.keywords,
pattern_options: self.pattern_options,
Expand Down
Loading