From 7a2ac3ef7ca35a6f9d26fa2b1890019d63a079f1 Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Wed, 22 Jan 2025 11:02:13 +0100 Subject: [PATCH] fix: Resolving external references nested inside local references Signed-off-by: Dmitry Dygalo --- CHANGELOG.md | 4 ++ crates/jsonschema-py/CHANGELOG.md | 4 ++ crates/jsonschema-referencing/src/registry.rs | 20 +++++++-- crates/jsonschema/src/keywords/ref_.rs | 44 +++++++++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c804cce2..028fa637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Resolving external references that nested inside local references. [#671](https://github.com/Stranger6667/jsonschema/issues/671) + ## [0.28.1] - 2024-12-31 ### Fixed diff --git a/crates/jsonschema-py/CHANGELOG.md b/crates/jsonschema-py/CHANGELOG.md index 0dd56b45..cfb1a442 100644 --- a/crates/jsonschema-py/CHANGELOG.md +++ b/crates/jsonschema-py/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Resolving external references that nested inside local references. [#671](https://github.com/Stranger6667/jsonschema/issues/671) + ## [0.28.1] - 2024-12-31 ### Fixed diff --git a/crates/jsonschema-referencing/src/registry.rs b/crates/jsonschema-referencing/src/registry.rs index 8f0a6158..b073d615 100644 --- a/crates/jsonschema-referencing/src/registry.rs +++ b/crates/jsonschema-referencing/src/registry.rs @@ -444,21 +444,32 @@ fn collect_external_resources( collected: &mut AHashSet>, seen: &mut AHashSet, ) -> Result<(), Error> { + // URN schemes are not supported for external resolution if base.scheme().as_str() == "urn" { return Ok(()); } for key in ["$ref", "$schema"] { if let Some(reference) = contents.get(key).and_then(Value::as_str) { - if reference.starts_with('#') - || reference.starts_with("https://json-schema.org/draft/2020-12/") + // Skip well-known schema references + if reference.starts_with("https://json-schema.org/draft/2020-12/") || reference.starts_with("https://json-schema.org/draft/2019-09/") || reference.starts_with("http://json-schema.org/draft-07/") || reference.starts_with("http://json-schema.org/draft-06/") || reference.starts_with("http://json-schema.org/draft-04/") { - // Not an external resource - return Ok(()); + continue; + } + // Handle local references separately as they may have nested references to external resources + if reference.starts_with('#') { + if reference == "#" { + continue; + } + if let Some(referenced) = contents.pointer(reference.trim_start_matches('#')) { + collect_external_resources(base, referenced, collected, seen)?; + } + continue; } + let mut hasher = AHasher::default(); (base.as_str(), reference).hash(&mut hasher); let hash = hasher.finish(); @@ -466,6 +477,7 @@ fn collect_external_resources( // Reference has already been seen return Ok(()); } + let resolved = if reference.contains('#') && base.has_fragment() { uri::resolve_against(&uri::DEFAULT_ROOT_URI.borrow(), reference)? } else { diff --git a/crates/jsonschema/src/keywords/ref_.rs b/crates/jsonschema/src/keywords/ref_.rs index 6d186e88..88fcf426 100644 --- a/crates/jsonschema/src/keywords/ref_.rs +++ b/crates/jsonschema/src/keywords/ref_.rs @@ -607,4 +607,48 @@ mod tests { let validator = crate::validator_for(&json!({"$ref": "#"})).expect("Invalid schema"); assert!(validator.is_valid(&json!(42))); } + + #[test] + fn test_nested_external_reference() { + let schema = json!({ + "$id": "foo://schema_1.json", + "$ref": "#/$defs/a/b", + "$defs": { + "a": { + "b": { + "description": "nested schema with external ref", + "$ref": "foo://schema_2.json" + } + } + } + }); + + struct NestedRetrieve; + + impl Retrieve for NestedRetrieve { + fn retrieve( + &self, + uri: &Uri<&str>, + ) -> Result> { + match uri.as_str() { + "foo://schema_2.json" => Ok(json!({ + "$id": "foo://schema_2.json", + "type": "string" + })), + _ => panic!("Unexpected URI: {}", uri.path()), + } + } + } + + let validator = match crate::options() + .with_retriever(NestedRetrieve) + .build(&schema) + { + Ok(validator) => validator, + Err(error) => panic!("Failed to build validator: {}", error), + }; + + assert!(validator.is_valid(&json!("test"))); + assert!(!validator.is_valid(&json!(42))); + } }