diff --git a/CHANGELOG.md b/CHANGELOG.md index 06b256ae..ad1d00cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ ## [Unreleased] +### Added + +- `ValidationError::evaluation_path()` returning the path including `$ref` traversals. +- `ValidationContext::custom_error()` for creating validation errors with correct `evaluation_path`. +- `ValidationError::schema()` for creating errors in keyword factory functions. + +### Changed + +- **BREAKING**: `Keyword::validate` now receives `ValidationContext` and `schema_path` parameters. +- **BREAKING**: `ValidationError::custom` is now internal. Use `ctx.custom_error()` or `ValidationError::schema()` instead. + +### Fixed + +- `schemaLocation` in evaluation output now excludes `$ref`/`$dynamicRef`/`$recursiveRef` per JSON Schema spec. + ## [0.37.4] - 2025-11-30 ### Fixed diff --git a/MIGRATION.md b/MIGRATION.md index d6875008..1e915624 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,5 +1,86 @@ # Migration Guide +## Upgrading from 0.37.x to 0.38.0 + +### Custom keyword API changes + +The custom keyword API now tracks `$ref` traversals to provide correct `evaluation_path` values in errors. +When a custom keyword is reached via `$ref`, the error's `evaluation_path` will include the `$ref` location, +while `schema_path` remains the canonical schema location. + +**`Keyword::validate` signature:** + +```rust +// Old (0.37.x) +fn validate<'i>( + &self, + instance: &'i Value, + location: &LazyLocation, +) -> Result<(), ValidationError<'i>>; + +// New (0.38.0) +fn validate<'i>( + &self, + instance: &'i Value, + instance_path: &LazyLocation, + ctx: &mut ValidationContext, + schema_path: &Location, +) -> Result<(), ValidationError<'i>>; +``` + +**Creating errors:** + +```rust +// Old (0.37.x) +ValidationError::custom(schema_path, instance_path, instance, message) + +// New (0.38.0) - for validation errors +ctx.custom_error(schema_path, instance_path, instance, message) + +// New (0.38.0) - for factory errors (invalid schema values) +ValidationError::schema(schema_path, schema_value, message) +``` + +**Updated implementation example:** + +```rust +use jsonschema::{Keyword, ValidationContext, ValidationError, paths::{LazyLocation, Location}}; +use serde_json::{Map, Value}; + +struct MyValidator; + +impl Keyword for MyValidator { + fn validate<'i>( + &self, + instance: &'i Value, + instance_path: &LazyLocation, + ctx: &mut ValidationContext, + schema_path: &Location, + ) -> Result<(), ValidationError<'i>> { + if !instance.is_string() { + return Err(ctx.custom_error(schema_path, instance_path, instance, "expected a string")); + } + Ok(()) + } + + fn is_valid(&self, instance: &Value) -> bool { + instance.is_string() + } +} + +fn my_keyword_factory<'a>( + _parent: &'a Map, + value: &'a Value, + schema_path: Location, +) -> Result, ValidationError<'a>> { + if value.as_bool() == Some(true) { + Ok(Box::new(MyValidator)) + } else { + Err(ValidationError::schema(schema_path, value, "expected true")) + } +} +``` + ## Upgrading from 0.36.x to 0.37.0 ### `ValidationError` is now opaque diff --git a/crates/jsonschema-py/CHANGELOG.md b/crates/jsonschema-py/CHANGELOG.md index 7d80573c..20a3e342 100644 --- a/crates/jsonschema-py/CHANGELOG.md +++ b/crates/jsonschema-py/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Added + +- `ValidationError.evaluation_path` attribute returning the path including `$ref` traversals. + +### Fixed + +- `schemaLocation` in evaluation output now excludes `$ref`/`$dynamicRef`/`$recursiveRef` per JSON Schema spec. + ## [0.37.4] - 2025-11-30 ### Fixed diff --git a/crates/jsonschema-py/python/jsonschema_rs/__init__.py b/crates/jsonschema-py/python/jsonschema_rs/__init__.py index 7e83f7fc..e415af25 100644 --- a/crates/jsonschema-py/python/jsonschema_rs/__init__.py +++ b/crates/jsonschema-py/python/jsonschema_rs/__init__.py @@ -36,6 +36,7 @@ class ValidationError(ValueError): verbose_message: str schema_path: list[str | int] instance_path: list[str | int] + evaluation_path: list[str | int] kind: ValidationErrorKind instance: Any @@ -45,6 +46,7 @@ def __init__( verbose_message: str, schema_path: list[str | int], instance_path: list[str | int], + evaluation_path: list[str | int], kind: ValidationErrorKind, instance: Any, ) -> None: @@ -53,6 +55,7 @@ def __init__( self.verbose_message = verbose_message self.schema_path = schema_path self.instance_path = instance_path + self.evaluation_path = evaluation_path self.kind = kind self.instance = instance diff --git a/crates/jsonschema-py/python/jsonschema_rs/__init__.pyi b/crates/jsonschema-py/python/jsonschema_rs/__init__.pyi index c1970236..d96e306f 100644 --- a/crates/jsonschema-py/python/jsonschema_rs/__init__.pyi +++ b/crates/jsonschema-py/python/jsonschema_rs/__init__.pyi @@ -255,6 +255,7 @@ class ValidationError(ValueError): verbose_message: str schema_path: list[str | int] instance_path: list[str | int] + evaluation_path: list[str | int] kind: ValidationErrorKind instance: JSONType diff --git a/crates/jsonschema-py/src/lib.rs b/crates/jsonschema-py/src/lib.rs index a0c03486..d82f1a78 100644 --- a/crates/jsonschema-py/src/lib.rs +++ b/crates/jsonschema-py/src/lib.rs @@ -216,6 +216,7 @@ struct ValidationErrorArgs { verbose_message: String, schema_path: Py, instance_path: Py, + evaluation_path: Py, kind: ValidationErrorKind, instance: Py, } @@ -231,6 +232,7 @@ fn create_validation_error_object( args.verbose_message, args.schema_path, args.instance_path, + args.evaluation_path, kind_obj, args.instance, ))?; @@ -410,8 +412,15 @@ impl ValidationErrorKind { jsonschema::error::ValidationErrorKind::PropertyNames { error } => { ValidationErrorKind::PropertyNames { error: { - let (message, verbose_message, schema_path, instance_path, kind, instance) = - into_validation_error_args(py, *error, mask)?; + let ( + message, + verbose_message, + schema_path, + instance_path, + evaluation_path, + kind, + instance, + ) = into_validation_error_args(py, *error, mask)?; create_validation_error_object( py, ValidationErrorArgs { @@ -419,6 +428,7 @@ impl ValidationErrorKind { verbose_message, schema_path, instance_path, + evaluation_path, kind, instance, }, @@ -476,8 +486,15 @@ fn convert_validation_context( let mut py_errors: Vec> = Vec::with_capacity(errors.len()); for error in errors { - let (message, verbose_message, schema_path, instance_path, kind, instance) = - into_validation_error_args(py, error, mask)?; + let ( + message, + verbose_message, + schema_path, + instance_path, + evaluation_path, + kind, + instance, + ) = into_validation_error_args(py, error, mask)?; py_errors.push(create_validation_error_object( py, @@ -486,6 +503,7 @@ fn convert_validation_context( verbose_message, schema_path, instance_path, + evaluation_path, kind, instance, }, @@ -523,6 +541,7 @@ fn into_validation_error_args( String, Py, Py, + Py, ValidationErrorKind, Py, )> { @@ -532,7 +551,7 @@ fn into_validation_error_args( error.to_string() }; let verbose_message = to_error_message(&error, message.clone(), mask); - let (instance, kind, instance_path, schema_path) = error.into_parts(); + let (instance, kind, instance_path, schema_path, evaluation_path) = error.into_parts(); let into_path = |segment: LocationSegment<'_>| match segment { LocationSegment::Property(property) => { property.into_pyobject(py).and_then(Py::::try_from) @@ -549,6 +568,11 @@ fn into_validation_error_args( .map(into_path) .collect::, _>>()?; let instance_path = PyList::new(py, elements)?.unbind(); + let elements = evaluation_path + .into_iter() + .map(into_path) + .collect::, _>>()?; + let evaluation_path = PyList::new(py, elements)?.unbind(); let kind = ValidationErrorKind::try_new(py, kind, mask)?; let instance = value_to_python(py, instance.as_ref())?; Ok(( @@ -556,6 +580,7 @@ fn into_validation_error_args( verbose_message, schema_path, instance_path, + evaluation_path, kind, instance, )) @@ -565,7 +590,7 @@ fn into_py_err( error: jsonschema::ValidationError<'_>, mask: Option<&str>, ) -> PyResult { - let (message, verbose_message, schema_path, instance_path, kind, instance) = + let (message, verbose_message, schema_path, instance_path, evaluation_path, kind, instance) = into_validation_error_args(py, error, mask)?; validation_error_pyerr( py, @@ -574,6 +599,7 @@ fn into_py_err( verbose_message, schema_path, instance_path, + evaluation_path, kind, instance, }, diff --git a/crates/jsonschema-py/tests-py/test_jsonschema.py b/crates/jsonschema-py/tests-py/test_jsonschema.py index 5405303b..101d7d30 100644 --- a/crates/jsonschema-py/tests-py/test_jsonschema.py +++ b/crates/jsonschema-py/tests-py/test_jsonschema.py @@ -201,6 +201,7 @@ def test_validation_error_kinds(schema, instance, kind, attrs): "", ["anyOf", 0, "type"], [], + ["anyOf", 0, "type"], ValidationErrorKind.Type(["string"]), True, ) @@ -211,6 +212,7 @@ def test_validation_error_kinds(schema, instance, kind, attrs): "", ["anyOf", 1, "type"], [], + ["anyOf", 1, "type"], ValidationErrorKind.Type(["number"]), True, ) @@ -228,6 +230,7 @@ def test_validation_error_kinds(schema, instance, kind, attrs): "", ["oneOf", 0, "type"], [], + ["oneOf", 0, "type"], ValidationErrorKind.Type(["number"]), "1", ) @@ -238,6 +241,7 @@ def test_validation_error_kinds(schema, instance, kind, attrs): "", ["oneOf", 1, "type"], [], + ["oneOf", 1, "type"], ValidationErrorKind.Type(["number"]), "1", ) diff --git a/crates/jsonschema-referencing/src/lib.rs b/crates/jsonschema-referencing/src/lib.rs index 3e9f2ed8..9b273ca6 100644 --- a/crates/jsonschema-referencing/src/lib.rs +++ b/crates/jsonschema-referencing/src/lib.rs @@ -17,7 +17,7 @@ mod vocabularies; pub(crate) use anchors::Anchor; pub use error::{Error, UriError}; -pub use fluent_uri::{Iri, IriRef, Uri, UriRef}; +pub use fluent_uri::{pct_enc::EStr, Iri, IriRef, Uri, UriRef}; pub use list::List; pub use registry::{parse_index, pointer, Registry, RegistryOptions, SPECIFICATIONS}; pub use resolver::{Resolved, Resolver}; diff --git a/crates/jsonschema/src/compiler.rs b/crates/jsonschema/src/compiler.rs index 35186bb1..b74bf506 100644 --- a/crates/jsonschema/src/compiler.rs +++ b/crates/jsonschema/src/compiler.rs @@ -890,7 +890,8 @@ fn compile_without_cache<'a>( // Check if this keyword is overridden, then check the standard definitions if let Some(factory) = ctx.get_keyword_factory(keyword) { let path = ctx.location().join(keyword); - let validator = CustomKeyword::new(factory.init(schema, value, path)?); + let validator = + CustomKeyword::new(factory.init(schema, value, path.clone())?, path); let validator: BoxedValidator = Box::new(validator); validators.push((Keyword::custom(keyword), validator)); } else if let Some((keyword, validator)) = keywords::get_for_draft(ctx, keyword) @@ -909,13 +910,17 @@ fn compile_without_cache<'a>( }; Ok(SchemaNode::from_keywords(ctx, validators, annotations)) } - _ => Err(ValidationError::multiple_type_error( - Location::new(), - ctx.location().clone(), - resource.contents(), - JsonTypeSet::empty() - .insert(JsonType::Boolean) - .insert(JsonType::Object), - )), + _ => { + let location = ctx.location().clone(); + Err(ValidationError::multiple_type_error( + location.clone(), + location, + Location::new(), + resource.contents(), + JsonTypeSet::empty() + .insert(JsonType::Boolean) + .insert(JsonType::Object), + )) + } } } diff --git a/crates/jsonschema/src/error.rs b/crates/jsonschema/src/error.rs index a49d5626..4af191e5 100644 --- a/crates/jsonschema/src/error.rs +++ b/crates/jsonschema/src/error.rs @@ -39,6 +39,7 @@ use crate::{ paths::Location, thread::ThreadBound, types::{JsonType, JsonTypeSet}, + validator::LazyEvaluationPath, }; use serde_json::{Map, Number, Value}; use std::{ @@ -62,7 +63,10 @@ struct ValidationErrorRepr<'a> { instance: Cow<'a, Value>, kind: ValidationErrorKind, instance_path: Location, + /// Canonical schema location without $ref traversals (JSON Schema "keywordLocation") schema_path: Location, + /// Dynamic path including $ref traversals. + evaluation_path: LazyEvaluationPath, } /// An iterator over instances of [`ValidationError`] that represent validation error for the @@ -380,6 +384,7 @@ impl<'a> ValidationError<'a> { kind: ValidationErrorKind, instance_path: Location, schema_path: Location, + evaluation_path: impl Into, ) -> Self { Self { repr: Box::new(ValidationErrorRepr { @@ -387,6 +392,7 @@ impl<'a> ValidationError<'a> { kind, instance_path, schema_path, + evaluation_path: evaluation_path.into(), }), } } @@ -412,23 +418,48 @@ impl<'a> ValidationError<'a> { &self.repr.instance_path } - /// Returns the JSON Pointer to the schema keyword that failed validation. + /// Returns the canonical schema location without `$ref` traversals. + /// + /// This corresponds to JSON Schema's "keywordLocation" in output formats. + /// See JSON Schema 2020-12 Core, Section 12.4.2. #[inline] #[must_use] pub fn schema_path(&self) -> &Location { &self.repr.schema_path } + /// Returns the dynamic evaluation path including `$ref` traversals. + /// + /// This corresponds to JSON Schema's "evaluationPath" - the actual path taken + /// through the schema including by-reference applicators (`$ref`, `$dynamicRef`). + /// See JSON Schema 2020-12 Core, Section 12.4.2. + #[inline] + #[must_use] + pub fn evaluation_path(&self) -> &Location { + self.repr.evaluation_path.resolve() + } + /// Decomposes the error into owned parts. + /// Returns (instance, kind, `instance_path`, `schema_path`, `evaluation_path`). #[inline] #[must_use] - pub fn into_parts(self) -> (Cow<'a, Value>, ValidationErrorKind, Location, Location) { + pub fn into_parts( + self, + ) -> ( + Cow<'a, Value>, + ValidationErrorKind, + Location, + Location, + Location, + ) { let repr = *self.repr; + // TODO: `clone` should not be needed ( repr.instance, repr.kind, repr.instance_path, repr.schema_path, + repr.evaluation_path.resolve().clone(), ) } @@ -438,8 +469,15 @@ impl<'a> ValidationError<'a> { kind: ValidationErrorKind, instance_path: Location, schema_path: Location, + evaluation_path: impl Into, ) -> Self { - Self::new(Cow::Borrowed(instance), kind, instance_path, schema_path) + Self::new( + Cow::Borrowed(instance), + kind, + instance_path, + schema_path, + evaluation_path, + ) } /// Returns a wrapper that masks instance values in error messages. @@ -462,17 +500,19 @@ impl<'a> ValidationError<'a> { /// Converts the `ValidationError` into an owned version with `'static` lifetime. #[must_use] pub fn to_owned(self) -> ValidationError<'static> { - let (instance, kind, instance_path, schema_path) = self.into_parts(); + let (instance, kind, instance_path, schema_path, evaluation_path) = self.into_parts(); ValidationError::new( Cow::Owned(instance.into_owned()), kind, instance_path, schema_path, + evaluation_path, ) } pub(crate) fn additional_items( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, limit: usize, @@ -481,11 +521,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::AdditionalItems { limit }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn additional_properties( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, unexpected: Vec, @@ -494,11 +536,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::AdditionalProperties { unexpected }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn any_of( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, context: Vec>>, @@ -512,11 +556,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::AnyOf { context }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn backtrack_limit( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, error: fancy_regex::Error, @@ -525,11 +571,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::BacktrackLimitExceeded { error }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn constant_array( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, expected_value: &[Value], @@ -540,11 +588,13 @@ impl<'a> ValidationError<'a> { expected_value: Value::Array(expected_value.to_vec()), }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn constant_boolean( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, expected_value: bool, @@ -555,11 +605,13 @@ impl<'a> ValidationError<'a> { expected_value: Value::Bool(expected_value), }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn constant_null( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, ) -> ValidationError<'a> { @@ -569,11 +621,13 @@ impl<'a> ValidationError<'a> { expected_value: Value::Null, }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn constant_number( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, expected_value: &Number, @@ -584,11 +638,13 @@ impl<'a> ValidationError<'a> { expected_value: Value::Number(expected_value.clone()), }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn constant_object( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, expected_value: &Map, @@ -599,11 +655,13 @@ impl<'a> ValidationError<'a> { expected_value: Value::Object(expected_value.clone()), }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn constant_string( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, expected_value: &str, @@ -614,11 +672,13 @@ impl<'a> ValidationError<'a> { expected_value: Value::String(expected_value.to_string()), }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn contains( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, ) -> ValidationError<'a> { @@ -626,11 +686,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::Contains, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn content_encoding( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, encoding: &str, @@ -641,11 +703,13 @@ impl<'a> ValidationError<'a> { content_encoding: encoding.to_string(), }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn content_media_type( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, media_type: &str, @@ -656,11 +720,13 @@ impl<'a> ValidationError<'a> { content_media_type: media_type.to_string(), }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn enumeration( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, options: &Value, @@ -671,11 +737,13 @@ impl<'a> ValidationError<'a> { options: options.clone(), }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn exclusive_maximum( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, limit: Value, @@ -684,11 +752,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::ExclusiveMaximum { limit }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn exclusive_minimum( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, limit: Value, @@ -697,11 +767,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::ExclusiveMinimum { limit }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn false_schema( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, ) -> ValidationError<'a> { @@ -709,11 +781,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::FalseSchema, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn format( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, format: impl Into, @@ -724,7 +798,8 @@ impl<'a> ValidationError<'a> { format: format.into(), }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn from_utf8(error: FromUtf8Error) -> ValidationError<'a> { @@ -733,10 +808,12 @@ impl<'a> ValidationError<'a> { ValidationErrorKind::FromUtf8 { error }, Location::new(), Location::new(), + Location::new(), ) } pub(crate) fn max_items( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, limit: u64, @@ -745,11 +822,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::MaxItems { limit }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn maximum( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, limit: Value, @@ -758,11 +837,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::Maximum { limit }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn max_length( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, limit: u64, @@ -771,11 +852,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::MaxLength { limit }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn max_properties( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, limit: u64, @@ -784,11 +867,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::MaxProperties { limit }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn min_items( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, limit: u64, @@ -797,11 +882,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::MinItems { limit }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn minimum( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, limit: Value, @@ -810,11 +897,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::Minimum { limit }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn min_length( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, limit: u64, @@ -823,11 +912,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::MinLength { limit }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn min_properties( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, limit: u64, @@ -836,12 +927,14 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::MinProperties { limit }, instance_path, - location, + schema_path, + evaluation_path, ) } #[cfg(feature = "arbitrary-precision")] pub(crate) fn multiple_of( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, multiple_of: Value, @@ -850,13 +943,15 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::MultipleOf { multiple_of }, instance_path, - location, + schema_path, + evaluation_path, ) } #[cfg(not(feature = "arbitrary-precision"))] pub(crate) fn multiple_of( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, multiple_of: f64, @@ -865,11 +960,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::MultipleOf { multiple_of }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn not( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, schema: Value, @@ -878,11 +975,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::Not { schema }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn one_of_multiple_valid( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, context: Vec>>, @@ -896,11 +995,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::OneOfMultipleValid { context }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn one_of_not_valid( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, context: Vec>>, @@ -914,11 +1015,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::OneOfNotValid { context }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn pattern( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, pattern: String, @@ -927,11 +1030,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::Pattern { pattern }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn property_names( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, error: ValidationError<'a>, @@ -942,11 +1047,13 @@ impl<'a> ValidationError<'a> { error: Box::new(error.to_owned()), }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn required( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, property: Value, @@ -955,12 +1062,14 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::Required { property }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn single_type_error( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, type_name: JsonType, @@ -971,11 +1080,13 @@ impl<'a> ValidationError<'a> { kind: TypeKind::Single(type_name), }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn multiple_type_error( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, types: JsonTypeSet, @@ -986,11 +1097,13 @@ impl<'a> ValidationError<'a> { kind: TypeKind::Multiple(types), }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn unevaluated_items( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, unexpected: Vec, @@ -999,11 +1112,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::UnevaluatedItems { unexpected }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn unevaluated_properties( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, unexpected: Vec, @@ -1012,11 +1127,13 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::UnevaluatedProperties { unexpected }, instance_path, - location, + schema_path, + evaluation_path, ) } pub(crate) fn unique_items( - location: Location, + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, ) -> ValidationError<'a> { @@ -1024,12 +1141,33 @@ impl<'a> ValidationError<'a> { instance, ValidationErrorKind::UniqueItems, instance_path, - location, + schema_path, + evaluation_path, + ) + } + /// Create a custom error for invalid schema values in keyword factories. + /// + /// Use this in factory functions when the schema value is invalid. + /// For validation errors, use [`crate::ValidationContext::custom_error`] instead. + pub fn schema( + schema_path: Location, + schema_value: &'a Value, + message: impl Into, + ) -> ValidationError<'a> { + Self::borrowed( + schema_value, + ValidationErrorKind::Custom { + message: message.into(), + }, + Location::new(), + schema_path.clone(), + schema_path, ) } - /// Create a new custom validation error. - pub fn custom( - location: Location, + + pub(crate) fn custom( + schema_path: Location, + evaluation_path: impl Into, instance_path: Location, instance: &'a Value, message: impl Into, @@ -1040,7 +1178,8 @@ impl<'a> ValidationError<'a> { message: message.into(), }, instance_path, - location, + schema_path, + evaluation_path, ) } } @@ -1054,6 +1193,7 @@ impl From for ValidationError<'_> { ValidationErrorKind::Referencing(err), Location::new(), Location::new(), + Location::new(), ) } } @@ -1528,7 +1668,13 @@ mod tests { use test_case::test_case; fn owned_error(instance: Value, kind: ValidationErrorKind) -> ValidationError<'static> { - ValidationError::new(Cow::Owned(instance), kind, Location::new(), Location::new()) + ValidationError::new( + Cow::Owned(instance), + kind, + Location::new(), + Location::new(), + Location::new(), + ) } #[test] @@ -1691,6 +1837,7 @@ mod tests { fn single_type_error() { let instance = json!(42); let err = ValidationError::single_type_error( + Location::new(), Location::new(), Location::new(), &instance, @@ -1706,6 +1853,7 @@ mod tests { .insert(JsonType::String) .insert(JsonType::Number); let err = ValidationError::multiple_type_error( + Location::new(), Location::new(), Location::new(), &instance, @@ -1873,8 +2021,13 @@ mod tests { "value is not of type \"string\"" )] fn test_masked_error_messages(instance: Value, kind: ValidationErrorKind, expected: &str) { - let error = - ValidationError::new(Cow::Owned(instance), kind, Location::new(), Location::new()); + let error = ValidationError::new( + Cow::Owned(instance), + kind, + Location::new(), + Location::new(), + Location::new(), + ); assert_eq!(error.masked().to_string(), expected); } @@ -1898,8 +2051,13 @@ mod tests { placeholder: &str, expected: &str, ) { - let error = - ValidationError::new(Cow::Owned(instance), kind, Location::new(), Location::new()); + let error = ValidationError::new( + Cow::Owned(instance), + kind, + Location::new(), + Location::new(), + Location::new(), + ); assert_eq!(error.masked_with(placeholder).to_string(), expected); } } diff --git a/crates/jsonschema/src/evaluation.rs b/crates/jsonschema/src/evaluation.rs index e6db187a..0386352e 100644 --- a/crates/jsonschema/src/evaluation.rs +++ b/crates/jsonschema/src/evaluation.rs @@ -626,10 +626,8 @@ pub(crate) fn format_schema_location( absolute: Option<&Arc>>, ) -> Arc { if let Some(uri) = absolute { - let base = uri.as_str(); - if base.contains('#') { - Arc::from(base) - } else if location.as_str().is_empty() { + let base = uri.strip_fragment(); + if location.as_str().is_empty() { Arc::from(format!("{base}#")) } else { Arc::from(format!("{base}#{}", location.as_str())) @@ -1568,10 +1566,10 @@ mod tests { .to_owned(), ); let formatted = format_schema_location(&location, Some(&uri)); - // When URI already contains a fragment, use it as-is + // When URI has a fragment, it's replaced with the location assert_eq!( formatted.as_ref(), - "http://example.com/schema.json#/defs/myDef" + "http://example.com/schema.json#/properties" ); } diff --git a/crates/jsonschema/src/keywords/additional_items.rs b/crates/jsonschema/src/keywords/additional_items.rs index 88df760e..bab43d11 100644 --- a/crates/jsonschema/src/keywords/additional_items.rs +++ b/crates/jsonschema/src/keywords/additional_items.rs @@ -3,9 +3,9 @@ use crate::{ error::{no_error, ErrorIterator, ValidationError}, keywords::{boolean::FalseValidator, CompilationResult}, node::SchemaNode, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, types::{JsonType, JsonTypeSet}, - validator::{Validate, ValidationContext}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -43,11 +43,13 @@ impl Validate for AdditionalItemsObjectValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Array(items) = instance { for (idx, item) in items.iter().enumerate().skip(self.items_count) { - self.node.validate(item, &location.push(idx), ctx)?; + self.node + .validate(item, &location.push(idx), evaluation_path, ctx)?; } } Ok(()) @@ -57,12 +59,18 @@ impl Validate for AdditionalItemsObjectValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Array(items) = instance { let mut errors = Vec::new(); for (idx, item) in items.iter().enumerate().skip(self.items_count) { - errors.extend(self.node.iter_errors(item, &location.push(idx), ctx)); + errors.extend(self.node.iter_errors( + item, + &location.push(idx), + evaluation_path, + ctx, + )); } ErrorIterator::from_iterator(errors.into_iter()) } else { @@ -98,12 +106,14 @@ impl Validate for AdditionalItemsBooleanValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, _ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Array(items) = instance { if items.len() > self.items_count { return Err(ValidationError::additional_items( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.items_count, @@ -147,15 +157,19 @@ pub(crate) fn compile<'a>( Some(FalseValidator::compile(location)) } } - _ => Some(Err(ValidationError::multiple_type_error( - Location::new(), - ctx.location().clone(), - schema, - JsonTypeSet::empty() - .insert(JsonType::Object) - .insert(JsonType::Array) - .insert(JsonType::Boolean), - ))), + _ => { + let location = ctx.location().join("additionalItems"); + Some(Err(ValidationError::multiple_type_error( + location.clone(), + location, + Location::new(), + schema, + JsonTypeSet::empty() + .insert(JsonType::Object) + .insert(JsonType::Array) + .insert(JsonType::Boolean), + ))) + } } } else { None diff --git a/crates/jsonschema/src/keywords/additional_properties.rs b/crates/jsonschema/src/keywords/additional_properties.rs index 4a3b6ad6..0a1a58ae 100644 --- a/crates/jsonschema/src/keywords/additional_properties.rs +++ b/crates/jsonschema/src/keywords/additional_properties.rs @@ -9,11 +9,11 @@ use crate::{ compiler, error::{no_error, ErrorIterator, ValidationError}, - evaluation::{format_schema_location, Annotations, ErrorDescription, EvaluationNode}, + evaluation::{Annotations, ErrorDescription, EvaluationNode}, keywords::CompilationResult, node::SchemaNode, options::PatternEngineOptions, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, properties::{ are_properties_valid, compile_big_map, compile_dynamic_prop_map_validator, compile_fancy_regex_patterns, compile_regex_patterns, compile_small_map, BigValidatorsMap, @@ -21,7 +21,7 @@ use crate::{ }, regex::RegexEngine, types::JsonType, - validator::{EvaluationResult, Validate, ValidationContext}, + validator::{capture_evaluation_path, EvaluationResult, Validate, ValidationContext}, }; use referencing::Uri; use serde_json::{Map, Value}; @@ -67,11 +67,13 @@ impl Validate for AdditionalPropertiesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { for (name, value) in item { - self.node.validate(value, &location.push(name), ctx)?; + self.node + .validate(value, &location.push(name), evaluation_path, ctx)?; } } Ok(()) @@ -81,15 +83,18 @@ impl Validate for AdditionalPropertiesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = instance { let mut errors = Vec::new(); for (name, value) in item { - errors.extend( - self.node - .iter_errors(value, &location.push(name.as_str()), ctx), - ); + errors.extend(self.node.iter_errors( + value, + &location.push(name.as_str()), + evaluation_path, + ctx, + )); } ErrorIterator::from_iterator(errors.into_iter()) } else { @@ -101,6 +106,7 @@ impl Validate for AdditionalPropertiesValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(item) = instance { @@ -109,6 +115,7 @@ impl Validate for AdditionalPropertiesValidator { children.push(self.node.evaluate_instance( value, &location.push(name.as_str()), + evaluation_path, ctx, )); } @@ -161,12 +168,14 @@ impl Validate for AdditionalPropertiesFalseValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, _ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { if let Some((_, value)) = item.iter().next() { return Err(ValidationError::false_schema( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), value, )); @@ -235,15 +244,17 @@ impl Validate for AdditionalPropertiesNotEmptyFalseV &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { for (property, value) in item { if let Some((name, node)) = self.properties.get_key_validator(property) { - node.validate(value, &location.push(name), ctx)?; + node.validate(value, &location.push(name), evaluation_path, ctx)?; } else { return Err(ValidationError::additional_properties( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, vec![property.clone()], @@ -258,6 +269,7 @@ impl Validate for AdditionalPropertiesNotEmptyFalseV &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = instance { @@ -265,7 +277,12 @@ impl Validate for AdditionalPropertiesNotEmptyFalseV let mut unexpected = vec![]; for (property, value) in item { if let Some((name, node)) = self.properties.get_key_validator(property) { - errors.extend(node.iter_errors(value, &location.push(name.as_str()), ctx)); + errors.extend(node.iter_errors( + value, + &location.push(name.as_str()), + evaluation_path, + ctx, + )); } else { unexpected.push(property.clone()); } @@ -273,6 +290,7 @@ impl Validate for AdditionalPropertiesNotEmptyFalseV if !unexpected.is_empty() { errors.push(ValidationError::additional_properties( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, unexpected, @@ -288,6 +306,7 @@ impl Validate for AdditionalPropertiesNotEmptyFalseV &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(item) = instance { @@ -298,6 +317,7 @@ impl Validate for AdditionalPropertiesNotEmptyFalseV children.push(node.evaluate_instance( value, &location.push(property.as_str()), + evaluation_path, ctx, )); } else { @@ -306,9 +326,11 @@ impl Validate for AdditionalPropertiesNotEmptyFalseV } let mut result = EvaluationResult::from_children(children); if !unexpected.is_empty() { + let evaluation_path = capture_evaluation_path(&self.location, evaluation_path); result.mark_errored(ErrorDescription::from_validation_error( &ValidationError::additional_properties( self.location.clone(), + evaluation_path, location.into(), instance, unexpected, @@ -388,15 +410,17 @@ impl Validate for AdditionalPropertiesNotEmptyValida &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(props) = instance { for (property, value) in props { let property_location = location.push(property); if let Some(validator) = self.properties.get_validator(property) { - validator.validate(value, &property_location, ctx)?; + validator.validate(value, &property_location, evaluation_path, ctx)?; } else { - self.node.validate(value, &property_location, ctx)?; + self.node + .validate(value, &property_location, evaluation_path, ctx)?; } } } @@ -407,6 +431,7 @@ impl Validate for AdditionalPropertiesNotEmptyValida &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(map) = instance { @@ -418,12 +443,14 @@ impl Validate for AdditionalPropertiesNotEmptyValida errors.extend(property_validators.iter_errors( value, &location.push(name.as_str()), + evaluation_path, ctx, )); } else { errors.extend(self.node.iter_errors( value, &location.push(property.as_str()), + evaluation_path, ctx, )); } @@ -438,6 +465,7 @@ impl Validate for AdditionalPropertiesNotEmptyValida &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(map) = instance { @@ -448,9 +476,17 @@ impl Validate for AdditionalPropertiesNotEmptyValida if let Some((_name, property_validators)) = self.properties.get_key_validator(property) { - children.push(property_validators.evaluate_instance(value, &path, ctx)); + children.push(property_validators.evaluate_instance( + value, + &path, + evaluation_path, + ctx, + )); } else { - children.push(self.node.evaluate_instance(value, &path, ctx)); + children.push( + self.node + .evaluate_instance(value, &path, evaluation_path, ctx), + ); matched_propnames.push(property.clone()); } } @@ -522,6 +558,7 @@ impl Validate for AdditionalPropertiesWithPatternsValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { @@ -531,11 +568,12 @@ impl Validate for AdditionalPropertiesWithPatternsValidator { for (re, node) in &self.patterns { if re.is_match(property).unwrap_or(false) { has_match = true; - node.validate(value, &property_location, ctx)?; + node.validate(value, &property_location, evaluation_path, ctx)?; } } if !has_match { - self.node.validate(value, &property_location, ctx)?; + self.node + .validate(value, &property_location, evaluation_path, ctx)?; } } } @@ -546,6 +584,7 @@ impl Validate for AdditionalPropertiesWithPatternsValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = instance { @@ -558,6 +597,7 @@ impl Validate for AdditionalPropertiesWithPatternsValidator { errors.extend(node.iter_errors( value, &location.push(property.as_str()), + evaluation_path, ctx, )); } @@ -566,6 +606,7 @@ impl Validate for AdditionalPropertiesWithPatternsValidator { errors.extend(self.node.iter_errors( value, &location.push(property.as_str()), + evaluation_path, ctx, )); } @@ -580,6 +621,7 @@ impl Validate for AdditionalPropertiesWithPatternsValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(item) = instance { @@ -593,17 +635,20 @@ impl Validate for AdditionalPropertiesWithPatternsValidator { if pattern.is_match(property).unwrap_or(false) { has_match = true; pattern_matched_propnames.push(property.clone()); - children.push(node.evaluate_instance(value, &path, ctx)); + children.push(node.evaluate_instance(value, &path, evaluation_path, ctx)); } } if !has_match { additional_matched_propnames.push(property.clone()); - children.push(self.node.evaluate_instance(value, &path, ctx)); + children.push( + self.node + .evaluate_instance(value, &path, evaluation_path, ctx), + ); } } if !pattern_matched_propnames.is_empty() { let annotation = Annotations::new(Value::from(pattern_matched_propnames)); - let schema_location = format_schema_location( + let schema_location = ctx.format_schema_location( &self.pattern_keyword_path, self.pattern_keyword_absolute_location.as_ref(), ); @@ -680,6 +725,7 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { @@ -689,12 +735,13 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator for (re, node) in &self.patterns { if re.is_match(property).unwrap_or(false) { has_match = true; - node.validate(value, &property_location, ctx)?; + node.validate(value, &property_location, evaluation_path, ctx)?; } } if !has_match { return Err(ValidationError::additional_properties( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, vec![property.clone()], @@ -709,6 +756,7 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = instance { @@ -722,6 +770,7 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator errors.extend(node.iter_errors( value, &location.push(property.as_str()), + evaluation_path, ctx, )); } @@ -733,6 +782,7 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator if !unexpected.is_empty() { errors.push(ValidationError::additional_properties( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, unexpected, @@ -748,6 +798,7 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(item) = instance { @@ -761,7 +812,7 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator if pattern.is_match(property).unwrap_or(false) { has_match = true; pattern_matched_props.push(property.clone()); - children.push(node.evaluate_instance(value, &path, ctx)); + children.push(node.evaluate_instance(value, &path, evaluation_path, ctx)); } } if !has_match { @@ -770,7 +821,7 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator } if !pattern_matched_props.is_empty() { let annotation = Annotations::new(Value::from(pattern_matched_props)); - let schema_location = format_schema_location( + let schema_location = ctx.format_schema_location( &self.pattern_keyword_path, self.pattern_keyword_absolute_location.as_ref(), ); @@ -785,9 +836,11 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator } let mut result = EvaluationResult::from_children(children); if !unexpected.is_empty() { + let evaluation_path = capture_evaluation_path(&self.location, evaluation_path); result.mark_errored(ErrorDescription::from_validation_error( &ValidationError::additional_properties( self.location.clone(), + evaluation_path, location.into(), instance, unexpected, @@ -876,16 +929,17 @@ impl Validate &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { for (property, value) in item { if let Some((name, node)) = self.properties.get_key_validator(property) { let name_location = location.push(name); - node.validate(value, &name_location, ctx)?; + node.validate(value, &name_location, evaluation_path, ctx)?; for (re, pattern_node) in &self.patterns { if re.is_match(property).unwrap_or(false) { - pattern_node.validate(value, &name_location, ctx)?; + pattern_node.validate(value, &name_location, evaluation_path, ctx)?; } } } else { @@ -894,11 +948,12 @@ impl Validate for (re, node) in &self.patterns { if re.is_match(property).unwrap_or(false) { has_match = true; - node.validate(value, &property_location, ctx)?; + node.validate(value, &property_location, evaluation_path, ctx)?; } } if !has_match { - self.node.validate(value, &property_location, ctx)?; + self.node + .validate(value, &property_location, evaluation_path, ctx)?; } } } @@ -910,18 +965,25 @@ impl Validate &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = instance { let mut errors = vec![]; for (property, value) in item { if let Some((name, node)) = self.properties.get_key_validator(property) { - errors.extend(node.iter_errors(value, &location.push(name.as_str()), ctx)); + errors.extend(node.iter_errors( + value, + &location.push(name.as_str()), + evaluation_path, + ctx, + )); for (re, pattern_node) in &self.patterns { if re.is_match(property).unwrap_or(false) { errors.extend(pattern_node.iter_errors( value, &location.push(name.as_str()), + evaluation_path, ctx, )); } @@ -934,6 +996,7 @@ impl Validate errors.extend(node.iter_errors( value, &location.push(property.as_str()), + evaluation_path, ctx, )); } @@ -942,6 +1005,7 @@ impl Validate errors.extend(self.node.iter_errors( value, &location.push(property.as_str()), + evaluation_path, ctx, )); } @@ -957,6 +1021,7 @@ impl Validate &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(item) = instance { @@ -965,10 +1030,15 @@ impl Validate for (property, value) in item { let path = location.push(property.as_str()); if let Some((_name, node)) = self.properties.get_key_validator(property) { - children.push(node.evaluate_instance(value, &path, ctx)); + children.push(node.evaluate_instance(value, &path, evaluation_path, ctx)); for (pattern, pattern_node) in &self.patterns { if pattern.is_match(property).unwrap_or(false) { - children.push(pattern_node.evaluate_instance(value, &path, ctx)); + children.push(pattern_node.evaluate_instance( + value, + &path, + evaluation_path, + ctx, + )); } } } else { @@ -976,12 +1046,22 @@ impl Validate for (pattern, node) in &self.patterns { if pattern.is_match(property).unwrap_or(false) { has_match = true; - children.push(node.evaluate_instance(value, &path, ctx)); + children.push(node.evaluate_instance( + value, + &path, + evaluation_path, + ctx, + )); } } if !has_match { additional_matches.push(property.clone()); - children.push(self.node.evaluate_instance(value, &path, ctx)); + children.push(self.node.evaluate_instance( + value, + &path, + evaluation_path, + ctx, + )); } } } @@ -1069,16 +1149,17 @@ impl Validate &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { for (property, value) in item { if let Some((name, node)) = self.properties.get_key_validator(property) { let name_location = location.push(name); - node.validate(value, &name_location, ctx)?; + node.validate(value, &name_location, evaluation_path, ctx)?; for (re, pattern_node) in &self.patterns { if re.is_match(property).unwrap_or(false) { - pattern_node.validate(value, &name_location, ctx)?; + pattern_node.validate(value, &name_location, evaluation_path, ctx)?; } } } else { @@ -1087,12 +1168,13 @@ impl Validate for (re, node) in &self.patterns { if re.is_match(property).unwrap_or(false) { has_match = true; - node.validate(value, &property_location, ctx)?; + node.validate(value, &property_location, evaluation_path, ctx)?; } } if !has_match { return Err(ValidationError::additional_properties( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, vec![property.clone()], @@ -1108,6 +1190,7 @@ impl Validate &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = instance { @@ -1115,12 +1198,18 @@ impl Validate let mut unexpected = vec![]; for (property, value) in item { if let Some((name, node)) = self.properties.get_key_validator(property) { - errors.extend(node.iter_errors(value, &location.push(name.as_str()), ctx)); + errors.extend(node.iter_errors( + value, + &location.push(name.as_str()), + evaluation_path, + ctx, + )); for (re, pattern_node) in &self.patterns { if re.is_match(property).unwrap_or(false) { errors.extend(pattern_node.iter_errors( value, &location.push(name.as_str()), + evaluation_path, ctx, )); } @@ -1133,6 +1222,7 @@ impl Validate errors.extend(node.iter_errors( value, &location.push(property.as_str()), + evaluation_path, ctx, )); } @@ -1145,6 +1235,7 @@ impl Validate if !unexpected.is_empty() { errors.push(ValidationError::additional_properties( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, unexpected, @@ -1160,6 +1251,7 @@ impl Validate &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(item) = instance { @@ -1168,10 +1260,15 @@ impl Validate for (property, value) in item { let path = location.push(property.as_str()); if let Some((_name, node)) = self.properties.get_key_validator(property) { - children.push(node.evaluate_instance(value, &path, ctx)); + children.push(node.evaluate_instance(value, &path, evaluation_path, ctx)); for (pattern, pattern_node) in &self.patterns { if pattern.is_match(property).unwrap_or(false) { - children.push(pattern_node.evaluate_instance(value, &path, ctx)); + children.push(pattern_node.evaluate_instance( + value, + &path, + evaluation_path, + ctx, + )); } } } else { @@ -1179,7 +1276,12 @@ impl Validate for (pattern, node) in &self.patterns { if pattern.is_match(property).unwrap_or(false) { has_match = true; - children.push(node.evaluate_instance(value, &path, ctx)); + children.push(node.evaluate_instance( + value, + &path, + evaluation_path, + ctx, + )); } } if !has_match { @@ -1189,9 +1291,11 @@ impl Validate } let mut result = EvaluationResult::from_children(children); if !unexpected.is_empty() { + let evaluation_path = capture_evaluation_path(&self.location, evaluation_path); result.mark_errored(ErrorDescription::from_validation_error( &ValidationError::additional_properties( self.location.clone(), + evaluation_path, location.into(), instance, unexpected, @@ -1296,8 +1400,10 @@ pub(crate) fn compile<'a>( ctx, map, patterns, ) } else { + let location = ctx.location().join("properties"); Some(Err(ValidationError::custom( - Location::new(), + location.clone(), + location, Location::new(), properties, "Unexpected type", @@ -1325,8 +1431,10 @@ pub(crate) fn compile<'a>( ctx, map, patterns, schema, ) } else { + let location = ctx.location().join("properties"); Some(Err(ValidationError::custom( - Location::new(), + location.clone(), + location, Location::new(), properties, "Unexpected type", @@ -1363,8 +1471,10 @@ pub(crate) fn compile<'a>( ctx, map, patterns, ) } else { + let location = ctx.location().join("properties"); Some(Err(ValidationError::custom( - Location::new(), + location.clone(), + location, Location::new(), properties, "Unexpected type", @@ -1392,8 +1502,10 @@ pub(crate) fn compile<'a>( ctx, map, patterns, schema, ) } else { + let location = ctx.location().join("properties"); Some(Err(ValidationError::custom( - Location::new(), + location.clone(), + location, Location::new(), properties, "Unexpected type", @@ -1418,10 +1530,12 @@ pub(crate) fn compile<'a>( } } } else { + let location = ctx.location().join("patternProperties"); Some(Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), - schema, + patterns, JsonType::Object, ))) } diff --git a/crates/jsonschema/src/keywords/all_of.rs b/crates/jsonschema/src/keywords/all_of.rs index 1f1f5643..d4dd88c1 100644 --- a/crates/jsonschema/src/keywords/all_of.rs +++ b/crates/jsonschema/src/keywords/all_of.rs @@ -2,7 +2,7 @@ use crate::{ compiler, error::{ErrorIterator, ValidationError}, node::SchemaNode, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, types::JsonType, validator::{EvaluationResult, Validate, ValidationContext}, }; @@ -40,10 +40,11 @@ impl Validate for AllOfValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { for schema in &self.schemas { - schema.validate(instance, location, ctx)?; + schema.validate(instance, location, evaluation_path, ctx)?; } Ok(()) } @@ -53,12 +54,13 @@ impl Validate for AllOfValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { let errors: Vec<_> = self .schemas .iter() - .flat_map(move |node| node.iter_errors(instance, location, ctx)) + .flat_map(move |node| node.iter_errors(instance, location, evaluation_path, ctx)) .collect(); ErrorIterator::from_iterator(errors.into_iter()) } @@ -67,13 +69,13 @@ impl Validate for AllOfValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { - let children = self - .schemas - .iter() - .map(move |node| node.evaluate_instance(instance, location, ctx)) - .collect(); + let mut children = Vec::with_capacity(self.schemas.len()); + for node in &self.schemas { + children.push(node.evaluate_instance(instance, location, evaluation_path, ctx)); + } EvaluationResult::from_children(children) } } @@ -101,27 +103,36 @@ impl Validate for SingleValueAllOfValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { - self.node.validate(instance, location, ctx) + self.node.validate(instance, location, evaluation_path, ctx) } fn iter_errors<'i>( &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { - self.node.iter_errors(instance, location, ctx) + self.node + .iter_errors(instance, location, evaluation_path, ctx) } fn evaluate( &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { - EvaluationResult::from(self.node.evaluate_instance(instance, location, ctx)) + EvaluationResult::from(self.node.evaluate_instance( + instance, + location, + evaluation_path, + ctx, + )) } } @@ -139,9 +150,11 @@ pub(crate) fn compile<'a>( Some(AllOfValidator::compile(ctx, items)) } } else { + let location = ctx.location().join("allOf"); Some(Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::Array, ))) diff --git a/crates/jsonschema/src/keywords/any_of.rs b/crates/jsonschema/src/keywords/any_of.rs index 19e63987..51e68119 100644 --- a/crates/jsonschema/src/keywords/any_of.rs +++ b/crates/jsonschema/src/keywords/any_of.rs @@ -2,9 +2,9 @@ use crate::{ compiler, error::{error, no_error, ErrorIterator, ValidationError}, node::SchemaNode, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, types::JsonType, - validator::{EvaluationResult, Validate, ValidationContext}, + validator::{capture_evaluation_path, EvaluationResult, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -31,9 +31,11 @@ impl AnyOfValidator { location: ctx.location().clone(), })) } else { + let location = ctx.location().join("anyOf"); Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::Array, )) @@ -50,6 +52,7 @@ impl Validate for AnyOfValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -57,11 +60,16 @@ impl Validate for AnyOfValidator { } else { Err(ValidationError::any_of( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.schemas .iter() - .map(|schema| schema.iter_errors(instance, location, ctx).collect()) + .map(|schema| { + schema + .iter_errors(instance, location, evaluation_path, ctx) + .collect() + }) .collect(), )) } @@ -71,6 +79,7 @@ impl Validate for AnyOfValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if self.is_valid(instance, ctx) { @@ -78,11 +87,16 @@ impl Validate for AnyOfValidator { } else { error(ValidationError::any_of( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.schemas .iter() - .map(|schema| schema.iter_errors(instance, location, ctx).collect()) + .map(|schema| { + schema + .iter_errors(instance, location, evaluation_path, ctx) + .collect() + }) .collect(), )) } @@ -92,28 +106,38 @@ impl Validate for AnyOfValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { - let total = self.schemas.len(); - let mut failures = Vec::with_capacity(total); - let mut iter = self.schemas.iter(); - while let Some(node) = iter.next() { - let result = node.evaluate_instance(instance, location, ctx); - if result.valid { - let remaining = total.saturating_sub(failures.len() + 1); - let mut successes = Vec::with_capacity(remaining + 1); - successes.push(result); - for node in iter { - let tail = node.evaluate_instance(instance, location, ctx); - if tail.valid { - successes.push(tail); - } - } - return EvaluationResult::from_children(successes); - } - failures.push(result); + // In most cases it is much faster to use cheap `is_valid` that does not build evaluation path and then + // re-run slower `evaluate` on a subset. It assumes that validation more often succeeds + // than not in real use cases. + let valid_indices: Vec = self + .schemas + .iter() + .enumerate() + .filter(|(_, node)| node.is_valid(instance, ctx)) + .map(|(idx, _)| idx) + .collect(); + + if valid_indices.is_empty() { + // No valid schemas - evaluate all for error output + let failures: Vec<_> = self + .schemas + .iter() + .map(|node| node.evaluate_instance(instance, location, evaluation_path, ctx)) + .collect(); + EvaluationResult::from_children(failures) + } else { + // At least one valid - only evaluate the valid ones + let successes: Vec<_> = valid_indices + .iter() + .map(|&idx| { + self.schemas[idx].evaluate_instance(instance, location, evaluation_path, ctx) + }) + .collect(); + EvaluationResult::from_children(successes) } - EvaluationResult::from_children(failures) } } diff --git a/crates/jsonschema/src/keywords/boolean.rs b/crates/jsonschema/src/keywords/boolean.rs index 61a28b05..b6429c14 100644 --- a/crates/jsonschema/src/keywords/boolean.rs +++ b/crates/jsonschema/src/keywords/boolean.rs @@ -1,9 +1,9 @@ -use crate::paths::{LazyLocation, Location}; +use crate::paths::{LazyLocation, LazyRefPath, Location}; use crate::{ error::ValidationError, keywords::CompilationResult, - validator::{Validate, ValidationContext}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::Value; @@ -25,10 +25,12 @@ impl Validate for FalseValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, _ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { Err(ValidationError::false_schema( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, )) diff --git a/crates/jsonschema/src/keywords/const_.rs b/crates/jsonschema/src/keywords/const_.rs index 7afb7ca6..9bd776f3 100644 --- a/crates/jsonschema/src/keywords/const_.rs +++ b/crates/jsonschema/src/keywords/const_.rs @@ -4,11 +4,11 @@ use crate::{ ext::cmp, keywords::CompilationResult, paths::Location, - validator::{Validate, ValidationContext}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Number, Value}; -use crate::paths::LazyLocation; +use crate::paths::{LazyLocation, LazyRefPath}; struct ConstArrayValidator { value: Vec, @@ -28,6 +28,7 @@ impl Validate for ConstArrayValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -35,6 +36,7 @@ impl Validate for ConstArrayValidator { } else { Err(ValidationError::constant_array( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, &self.value, @@ -67,6 +69,7 @@ impl Validate for ConstBooleanValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -74,6 +77,7 @@ impl Validate for ConstBooleanValidator { } else { Err(ValidationError::constant_boolean( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.value, @@ -105,6 +109,7 @@ impl Validate for ConstNullValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -112,6 +117,7 @@ impl Validate for ConstNullValidator { } else { Err(ValidationError::constant_null( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, )) @@ -144,6 +150,7 @@ impl Validate for ConstNumberValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -151,6 +158,7 @@ impl Validate for ConstNumberValidator { } else { Err(ValidationError::constant_number( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, &self.original_value, @@ -188,6 +196,7 @@ impl Validate for ConstObjectValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -195,6 +204,7 @@ impl Validate for ConstObjectValidator { } else { Err(ValidationError::constant_object( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, &self.value, @@ -232,6 +242,7 @@ impl Validate for ConstStringValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -239,6 +250,7 @@ impl Validate for ConstStringValidator { } else { Err(ValidationError::constant_string( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, &self.value, diff --git a/crates/jsonschema/src/keywords/contains.rs b/crates/jsonschema/src/keywords/contains.rs index ae0b4571..a9045060 100644 --- a/crates/jsonschema/src/keywords/contains.rs +++ b/crates/jsonschema/src/keywords/contains.rs @@ -4,8 +4,8 @@ use crate::{ evaluation::{Annotations, ErrorDescription}, keywords::CompilationResult, node::SchemaNode, - paths::LazyLocation, - validator::{EvaluationResult, Validate, ValidationContext}, + paths::{LazyLocation, LazyRefPath}, + validator::{capture_evaluation_path, EvaluationResult, Validate, ValidationContext}, Draft, }; use serde_json::{Map, Value}; @@ -39,6 +39,7 @@ impl Validate for ContainsValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Array(items) = instance { @@ -47,6 +48,7 @@ impl Validate for ContainsValidator { } Err(ValidationError::contains( self.node.location().clone(), + capture_evaluation_path(self.node.location(), evaluation_path), location.into(), instance, )) @@ -59,6 +61,7 @@ impl Validate for ContainsValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Array(items) = instance { @@ -66,17 +69,22 @@ impl Validate for ContainsValidator { let mut indices = Vec::with_capacity(items.len()); for (idx, item) in items.iter().enumerate() { let path = location.push(idx); - let result = self.node.evaluate_instance(item, &path, ctx); + let result = self + .node + .evaluate_instance(item, &path, evaluation_path, ctx); if result.valid { indices.push(idx); results.push(result); } } if indices.is_empty() { + let evaluation_path = + capture_evaluation_path(self.node.location(), evaluation_path); EvaluationResult::Invalid { errors: vec![ErrorDescription::from_validation_error( &ValidationError::contains( self.node.location().clone(), + evaluation_path, location.into(), instance, ), @@ -147,6 +155,7 @@ impl Validate for MinContainsValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Array(items) = instance { @@ -166,6 +175,7 @@ impl Validate for MinContainsValidator { if self.min_contains > 0 { Err(ValidationError::contains( self.node.location().clone(), + capture_evaluation_path(self.node.location(), evaluation_path), location.into(), instance, )) @@ -227,6 +237,7 @@ impl Validate for MaxContainsValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Array(items) = instance { @@ -241,6 +252,7 @@ impl Validate for MaxContainsValidator { if matches > self.max_contains { return Err(ValidationError::contains( self.node.location().clone(), + capture_evaluation_path(self.node.location(), evaluation_path), location.into(), instance, )); @@ -252,6 +264,7 @@ impl Validate for MaxContainsValidator { } else { Err(ValidationError::contains( self.node.location().clone(), + capture_evaluation_path(self.node.location(), evaluation_path), location.into(), instance, )) @@ -315,6 +328,7 @@ impl Validate for MinMaxContainsValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Array(items) = instance { @@ -327,8 +341,10 @@ impl Validate for MinMaxContainsValidator { { matches += 1; if matches > self.max_contains { + let max_location = self.node.location().join("maxContains"); return Err(ValidationError::contains( - self.node.location().join("maxContains"), + max_location.clone(), + capture_evaluation_path(&max_location, evaluation_path), location.into(), instance, )); @@ -336,8 +352,10 @@ impl Validate for MinMaxContainsValidator { } } if matches < self.min_contains { + let min_location = self.node.location().join("minContains"); Err(ValidationError::contains( - self.node.location().join("minContains"), + min_location.clone(), + capture_evaluation_path(&min_location, evaluation_path), location.into(), instance, )) diff --git a/crates/jsonschema/src/keywords/content.rs b/crates/jsonschema/src/keywords/content.rs index f6177e17..5ffb7327 100644 --- a/crates/jsonschema/src/keywords/content.rs +++ b/crates/jsonschema/src/keywords/content.rs @@ -5,9 +5,9 @@ use crate::{ content_media_type::ContentMediaTypeCheckType, error::ValidationError, keywords::CompilationResult, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, types::JsonType, - validator::{Validate, ValidationContext}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -47,6 +47,7 @@ impl Validate for ContentMediaTypeValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -54,6 +55,7 @@ impl Validate for ContentMediaTypeValidator { } else if let Value::String(_) = instance { Err(ValidationError::content_media_type( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, &self.media_type, @@ -99,6 +101,7 @@ impl Validate for ContentEncodingValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -106,6 +109,7 @@ impl Validate for ContentEncodingValidator { } else if let Value::String(_) = instance { Err(ValidationError::content_encoding( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, &self.encoding, @@ -161,12 +165,16 @@ impl Validate for ContentMediaTypeAndEncodingValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, _ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::String(item) = instance { + let encoding_location = self.location.join("contentEncoding"); + let media_type_location = self.location.join("contentMediaType"); match (self.converter)(item) { Ok(None) => Err(ValidationError::content_encoding( - self.location.join("contentEncoding"), + encoding_location.clone(), + capture_evaluation_path(&encoding_location, evaluation_path), location.into(), instance, &self.encoding, @@ -176,7 +184,8 @@ impl Validate for ContentMediaTypeAndEncodingValidator { Ok(()) } else { Err(ValidationError::content_media_type( - self.location.join("contentMediaType"), + media_type_location.clone(), + capture_evaluation_path(&media_type_location, evaluation_path), location.into(), instance, &self.media_type, @@ -197,42 +206,44 @@ pub(crate) fn compile_media_type<'a>( schema: &'a Map, subschema: &'a Value, ) -> Option> { - match subschema { - Value::String(media_type) => { - let func = ctx.get_content_media_type_check(media_type.as_str())?; - if let Some(content_encoding) = schema.get("contentEncoding") { - match content_encoding { - Value::String(content_encoding) => { - let converter = ctx.get_content_encoding_convert(content_encoding)?; - Some(ContentMediaTypeAndEncodingValidator::compile( - media_type, - content_encoding, - func, - converter, - ctx.location().clone(), - )) - } - _ => Some(Err(ValidationError::single_type_error( - Location::new(), - ctx.location().clone(), - content_encoding, - JsonType::String, - ))), - } - } else { - Some(ContentMediaTypeValidator::compile( + if let Value::String(media_type) = subschema { + let func = ctx.get_content_media_type_check(media_type.as_str())?; + if let Some(content_encoding) = schema.get("contentEncoding") { + if let Value::String(content_encoding) = content_encoding { + let converter = ctx.get_content_encoding_convert(content_encoding)?; + Some(ContentMediaTypeAndEncodingValidator::compile( media_type, + content_encoding, func, - ctx.location().join("contentMediaType"), + converter, + ctx.location().clone(), )) + } else { + let location = ctx.location().join("contentEncoding"); + Some(Err(ValidationError::single_type_error( + location.clone(), + location, + Location::new(), + content_encoding, + JsonType::String, + ))) } + } else { + Some(ContentMediaTypeValidator::compile( + media_type, + func, + ctx.location().join("contentMediaType"), + )) } - _ => Some(Err(ValidationError::single_type_error( + } else { + let location = ctx.location().join("contentMediaType"); + Some(Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), subschema, JsonType::String, - ))), + ))) } } @@ -247,21 +258,22 @@ pub(crate) fn compile_content_encoding<'a>( // TODO. what if media type is not supported? return None; } - match subschema { - Value::String(content_encoding) => { - let func = ctx.get_content_encoding_check(content_encoding)?; - Some(ContentEncodingValidator::compile( - content_encoding, - func, - ctx.location().join("contentEncoding"), - )) - } - _ => Some(Err(ValidationError::single_type_error( + if let Value::String(content_encoding) = subschema { + let func = ctx.get_content_encoding_check(content_encoding)?; + Some(ContentEncodingValidator::compile( + content_encoding, + func, + ctx.location().join("contentEncoding"), + )) + } else { + let location = ctx.location().join("contentEncoding"); + Some(Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), subschema, JsonType::String, - ))), + ))) } } diff --git a/crates/jsonschema/src/keywords/custom.rs b/crates/jsonschema/src/keywords/custom.rs index c0c6a34e..23e8b18a 100644 --- a/crates/jsonschema/src/keywords/custom.rs +++ b/crates/jsonschema/src/keywords/custom.rs @@ -1,5 +1,5 @@ use crate::{ - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, thread::ThreadBound, validator::{Validate, ValidationContext}, ValidationError, @@ -8,11 +8,12 @@ use serde_json::{Map, Value}; pub(crate) struct CustomKeyword { inner: Box, + location: Location, } impl CustomKeyword { - pub(crate) fn new(inner: Box) -> Self { - Self { inner } + pub(crate) fn new(inner: Box, location: Location) -> Self { + Self { inner, location } } } @@ -20,10 +21,12 @@ impl Validate for CustomKeyword { fn validate<'i>( &self, instance: &'i Value, - location: &LazyLocation, - _ctx: &mut ValidationContext, + instance_path: &LazyLocation, + _evaluation_path: &LazyRefPath, + ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { - self.inner.validate(instance, location) + self.inner + .validate(instance, instance_path, ctx, &self.location) } fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool { @@ -31,26 +34,64 @@ impl Validate for CustomKeyword { } } -/// Trait that allows implementing custom validation for keywords. +/// Trait for implementing custom keyword validators. +/// +/// Custom keywords extend JSON Schema validation with domain-specific rules. +/// +/// # Example +/// +/// ```rust +/// use jsonschema::{ +/// paths::{LazyLocation, Location}, +/// Keyword, ValidationContext, ValidationError, +/// }; +/// use serde_json::Value; +/// +/// struct EvenNumberValidator; +/// +/// impl Keyword for EvenNumberValidator { +/// fn validate<'i>( +/// &self, +/// instance: &'i Value, +/// instance_path: &LazyLocation, +/// ctx: &mut ValidationContext, +/// schema_path: &Location, +/// ) -> Result<(), ValidationError<'i>> { +/// if let Some(n) = instance.as_u64() { +/// if n % 2 != 0 { +/// return Err(ctx.custom_error( +/// schema_path, +/// instance_path, +/// instance, +/// "number must be even", +/// )); +/// } +/// } +/// Ok(()) +/// } +/// +/// fn is_valid(&self, instance: &Value) -> bool { +/// instance.as_u64().map_or(true, |n| n % 2 == 0) +/// } +/// } +/// ``` pub trait Keyword: ThreadBound { - /// Validate instance according to a custom specification. + /// Validate an instance against this custom keyword. /// - /// A custom keyword validator may be used when a validation that cannot be - /// easily or efficiently expressed in JSON schema. - /// - /// The custom validation is applied in addition to the JSON schema validation. + /// Use `ctx.custom_error()` to create errors with correct `evaluation_path`. /// /// # Errors /// - /// Returns an error describing why `instance` violates the custom keyword semantics. + /// Returns a [`ValidationError`] if the instance fails validation. fn validate<'i>( &self, instance: &'i Value, - location: &LazyLocation, + instance_path: &LazyLocation, + ctx: &mut ValidationContext, + schema_path: &Location, ) -> Result<(), ValidationError<'i>>; - /// Validate instance and return a boolean result. - /// - /// Could be potentilly faster than [`Keyword::validate`] method. + + /// Check validity without collecting error details. fn is_valid(&self, instance: &Value) -> bool; } @@ -59,7 +100,7 @@ pub(crate) trait KeywordFactory: ThreadBound { &self, parent: &'a Map, schema: &'a Value, - path: Location, + schema_path: Location, ) -> Result, ValidationError<'a>>; } @@ -76,8 +117,8 @@ where &self, parent: &'a Map, schema: &'a Value, - path: Location, + schema_path: Location, ) -> Result, ValidationError<'a>> { - self(parent, schema, path) + self(parent, schema, schema_path) } } diff --git a/crates/jsonschema/src/keywords/dependencies.rs b/crates/jsonschema/src/keywords/dependencies.rs index 910a88d5..5141ba2b 100644 --- a/crates/jsonschema/src/keywords/dependencies.rs +++ b/crates/jsonschema/src/keywords/dependencies.rs @@ -3,7 +3,7 @@ use crate::{ error::{no_error, ErrorIterator, ValidationError}, keywords::{required, unique_items, CompilationResult}, node::SchemaNode, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, types::JsonType, validator::{EvaluationResult, Validate, ValidationContext}, }; @@ -37,9 +37,11 @@ impl DependenciesValidator { } Ok(Box::new(DependenciesValidator { dependencies })) } else { + let location = ctx.location().join("dependencies"); Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::Object, )) @@ -65,12 +67,13 @@ impl Validate for DependenciesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { for (property, dependency) in &self.dependencies { if item.contains_key(property) { - dependency.validate(instance, location, ctx)?; + dependency.validate(instance, location, evaluation_path, ctx)?; } } } @@ -81,13 +84,14 @@ impl Validate for DependenciesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = instance { let mut errors = Vec::new(); for (property, node) in &self.dependencies { if item.contains_key(property) { - errors.extend(node.iter_errors(instance, location, ctx)); + errors.extend(node.iter_errors(instance, location, evaluation_path, ctx)); } } ErrorIterator::from_iterator(errors.into_iter()) @@ -100,13 +104,19 @@ impl Validate for DependenciesValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(item) = instance { let mut children = Vec::new(); for (property, dependency) in &self.dependencies { if item.contains_key(property) { - children.push(dependency.evaluate_instance(instance, location, ctx)); + children.push(dependency.evaluate_instance( + instance, + location, + evaluation_path, + ctx, + )); } } EvaluationResult::from_children(children) @@ -130,9 +140,11 @@ impl DependentRequiredValidator { let ictx = kctx.new_at_location(key.as_str()); if let Value::Array(dependency_array) = subschema { if !unique_items::is_unique(dependency_array) { + let location = ictx.location().clone(); return Err(ValidationError::unique_items( + location.clone(), + location, Location::new(), - ictx.location().clone(), subschema, )); } @@ -145,9 +157,11 @@ impl DependentRequiredValidator { ]; dependencies.push((key.clone(), SchemaNode::from_array(&kctx, validators))); } else { + let location = ictx.location().clone(); return Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ictx.location().clone(), subschema, JsonType::Array, )); @@ -155,9 +169,11 @@ impl DependentRequiredValidator { } Ok(Box::new(DependentRequiredValidator { dependencies })) } else { + let location = ctx.location().join("dependentRequired"); Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::Object, )) @@ -182,12 +198,13 @@ impl Validate for DependentRequiredValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { for (property, dependency) in &self.dependencies { if item.contains_key(property) { - dependency.validate(instance, location, ctx)?; + dependency.validate(instance, location, evaluation_path, ctx)?; } } } @@ -198,13 +215,14 @@ impl Validate for DependentRequiredValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = instance { let mut errors = Vec::new(); for (property, node) in &self.dependencies { if item.contains_key(property) { - errors.extend(node.iter_errors(instance, location, ctx)); + errors.extend(node.iter_errors(instance, location, evaluation_path, ctx)); } } ErrorIterator::from_iterator(errors.into_iter()) @@ -217,13 +235,19 @@ impl Validate for DependentRequiredValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(item) = instance { let mut children = Vec::new(); for (property, dependency) in &self.dependencies { if item.contains_key(property) { - children.push(dependency.evaluate_instance(instance, location, ctx)); + children.push(dependency.evaluate_instance( + instance, + location, + evaluation_path, + ctx, + )); } } EvaluationResult::from_children(children) @@ -249,9 +273,11 @@ impl DependentSchemasValidator { } Ok(Box::new(DependentSchemasValidator { dependencies })) } else { + let location = ctx.location().join("dependentSchemas"); Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::Object, )) @@ -276,12 +302,13 @@ impl Validate for DependentSchemasValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { for (property, dependency) in &self.dependencies { if item.contains_key(property) { - dependency.validate(instance, location, ctx)?; + dependency.validate(instance, location, evaluation_path, ctx)?; } } } @@ -292,13 +319,14 @@ impl Validate for DependentSchemasValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = instance { let mut errors = Vec::new(); for (property, node) in &self.dependencies { if item.contains_key(property) { - errors.extend(node.iter_errors(instance, location, ctx)); + errors.extend(node.iter_errors(instance, location, evaluation_path, ctx)); } } ErrorIterator::from_iterator(errors.into_iter()) @@ -311,13 +339,19 @@ impl Validate for DependentSchemasValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(item) = instance { let mut children = Vec::new(); for (property, dependency) in &self.dependencies { if item.contains_key(property) { - children.push(dependency.evaluate_instance(instance, location, ctx)); + children.push(dependency.evaluate_instance( + instance, + location, + evaluation_path, + ctx, + )); } } EvaluationResult::from_children(children) diff --git a/crates/jsonschema/src/keywords/enum_.rs b/crates/jsonschema/src/keywords/enum_.rs index 3267ece3..7a5b4be8 100644 --- a/crates/jsonschema/src/keywords/enum_.rs +++ b/crates/jsonschema/src/keywords/enum_.rs @@ -3,9 +3,9 @@ use crate::{ error::ValidationError, ext::cmp, keywords::CompilationResult, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, types::{JsonType, JsonTypeSet}, - validator::{Validate, ValidationContext}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -43,6 +43,7 @@ impl Validate for EnumValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -50,6 +51,7 @@ impl Validate for EnumValidator { } else { Err(ValidationError::enumeration( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, &self.options, @@ -96,6 +98,7 @@ impl Validate for SingleValueEnumValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -103,6 +106,7 @@ impl Validate for SingleValueEnumValidator { } else { Err(ValidationError::enumeration( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, &self.options, @@ -130,9 +134,11 @@ pub(crate) fn compile<'a>( Some(EnumValidator::compile(schema, items, location)) } } else { + let location = ctx.location().join("enum"); Some(Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::Array, ))) diff --git a/crates/jsonschema/src/keywords/format.rs b/crates/jsonschema/src/keywords/format.rs index a8c5070e..61a9c126 100644 --- a/crates/jsonschema/src/keywords/format.rs +++ b/crates/jsonschema/src/keywords/format.rs @@ -16,10 +16,10 @@ use crate::{ compiler, ecma, error::ValidationError, keywords::CompilationResult, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, thread::ThreadBound, types::JsonType, - validator::{Validate, ValidationContext}, + validator::{capture_evaluation_path, Validate, ValidationContext}, Draft, }; @@ -722,7 +722,7 @@ macro_rules! format_validators { } impl Validate for $validator { - fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool { + fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool { if let Value::String(item) = instance { $validation_fn(item) } else { @@ -734,12 +734,14 @@ macro_rules! format_validators { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::String(_item) = instance { if !self.is_valid(instance, ctx) { return Err(ValidationError::format( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, $format, @@ -814,12 +816,14 @@ impl Validate for EmailValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::String(_item) = instance { if !self.is_valid(instance, ctx) { return Err(ValidationError::format( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, "email", @@ -860,12 +864,14 @@ impl Validate for IdnEmailValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::String(_item) = instance { if !self.is_valid(instance, ctx) { return Err(ValidationError::format( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, "idn-email", @@ -901,6 +907,7 @@ impl Validate for CustomFormatValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -908,6 +915,7 @@ impl Validate for CustomFormatValidator { } else { Err(ValidationError::format( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.format_name.clone(), @@ -983,9 +991,11 @@ pub(crate) fn compile<'a>( if ctx.are_unknown_formats_ignored() { None } else { + let location = ctx.location().join("format"); Some(Err(ValidationError::custom( - Location::new().join("format"), - ctx.location().clone(), + location.clone(), + location, + Location::new(), schema, format!("Unknown format: '{name}'. Adjust configuration to ignore unrecognized formats"), ))) @@ -993,9 +1003,11 @@ pub(crate) fn compile<'a>( } } } else { + let location = ctx.location().join("format"); Some(Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::String, ))) diff --git a/crates/jsonschema/src/keywords/helpers.rs b/crates/jsonschema/src/keywords/helpers.rs index dabd2fc6..8cad322f 100644 --- a/crates/jsonschema/src/keywords/helpers.rs +++ b/crates/jsonschema/src/keywords/helpers.rs @@ -13,12 +13,16 @@ pub(crate) fn map_get_u64<'a>( let value = m.get(type_name)?; match value.as_u64() { Some(n) => Some(Ok(n)), - None if value.is_i64() => Some(Err(ValidationError::minimum( - Location::new(), - ctx.location().clone(), - value, - 0.into(), - ))), + None if value.is_i64() => { + let location = ctx.location().join(type_name); + Some(Err(ValidationError::minimum( + location.clone(), + location, + Location::new(), + value, + 0.into(), + ))) + } None => { if let Some(value) = value.as_f64() { if value.trunc() == value { @@ -27,9 +31,11 @@ pub(crate) fn map_get_u64<'a>( return Some(Ok(value as u64)); } } + let location = ctx.location().join(type_name); Some(Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), value, JsonType::Integer, ))) @@ -43,8 +49,20 @@ pub(crate) fn fail_on_non_positive_integer( instance_path: Location, ) -> ValidationError<'_> { if value.is_i64() { - ValidationError::minimum(Location::new(), instance_path, value, 0.into()) + ValidationError::minimum( + instance_path.clone(), + instance_path, + Location::new(), + value, + 0.into(), + ) } else { - ValidationError::single_type_error(Location::new(), instance_path, value, JsonType::Integer) + ValidationError::single_type_error( + instance_path.clone(), + instance_path, + Location::new(), + value, + JsonType::Integer, + ) } } diff --git a/crates/jsonschema/src/keywords/if_.rs b/crates/jsonschema/src/keywords/if_.rs index 67d95b50..648b65b6 100644 --- a/crates/jsonschema/src/keywords/if_.rs +++ b/crates/jsonschema/src/keywords/if_.rs @@ -3,7 +3,7 @@ use crate::{ error::{no_error, ErrorIterator}, keywords::CompilationResult, node::SchemaNode, - paths::LazyLocation, + paths::{LazyLocation, LazyRefPath}, validator::{EvaluationResult, Validate, ValidationContext}, ValidationError, }; @@ -47,10 +47,12 @@ impl Validate for IfThenValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.schema.is_valid(instance, ctx) { - self.then_schema.validate(instance, location, ctx) + self.then_schema + .validate(instance, location, evaluation_path, ctx) } else { Ok(()) } @@ -61,12 +63,13 @@ impl Validate for IfThenValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if self.schema.is_valid(instance, ctx) { let errors: Vec<_> = self .then_schema - .iter_errors(instance, location, ctx) + .iter_errors(instance, location, evaluation_path, ctx) .collect(); ErrorIterator::from_iterator(errors.into_iter()) } else { @@ -78,11 +81,16 @@ impl Validate for IfThenValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { - let if_node = self.schema.evaluate_instance(instance, location, ctx); + let if_node = self + .schema + .evaluate_instance(instance, location, evaluation_path, ctx); if if_node.valid { - let then_node = self.then_schema.evaluate_instance(instance, location, ctx); + let then_node = + self.then_schema + .evaluate_instance(instance, location, evaluation_path, ctx); EvaluationResult::from_children(vec![if_node, then_node]) } else { EvaluationResult::valid_empty() @@ -128,12 +136,14 @@ impl Validate for IfElseValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.schema.is_valid(instance, ctx) { Ok(()) } else { - self.else_schema.validate(instance, location, ctx) + self.else_schema + .validate(instance, location, evaluation_path, ctx) } } @@ -142,6 +152,7 @@ impl Validate for IfElseValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if self.schema.is_valid(instance, ctx) { @@ -149,7 +160,7 @@ impl Validate for IfElseValidator { } else { let errors: Vec<_> = self .else_schema - .iter_errors(instance, location, ctx) + .iter_errors(instance, location, evaluation_path, ctx) .collect(); ErrorIterator::from_iterator(errors.into_iter()) } @@ -159,13 +170,18 @@ impl Validate for IfElseValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { - let if_node = self.schema.evaluate_instance(instance, location, ctx); + let if_node = self + .schema + .evaluate_instance(instance, location, evaluation_path, ctx); if if_node.valid { EvaluationResult::from_children(vec![if_node]) } else { - let else_node = self.else_schema.evaluate_instance(instance, location, ctx); + let else_node = + self.else_schema + .evaluate_instance(instance, location, evaluation_path, ctx); EvaluationResult::from_children(vec![else_node]) } } @@ -215,12 +231,15 @@ impl Validate for IfThenElseValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.schema.is_valid(instance, ctx) { - self.then_schema.validate(instance, location, ctx) + self.then_schema + .validate(instance, location, evaluation_path, ctx) } else { - self.else_schema.validate(instance, location, ctx) + self.else_schema + .validate(instance, location, evaluation_path, ctx) } } @@ -229,18 +248,19 @@ impl Validate for IfThenElseValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if self.schema.is_valid(instance, ctx) { let errors: Vec<_> = self .then_schema - .iter_errors(instance, location, ctx) + .iter_errors(instance, location, evaluation_path, ctx) .collect(); ErrorIterator::from_iterator(errors.into_iter()) } else { let errors: Vec<_> = self .else_schema - .iter_errors(instance, location, ctx) + .iter_errors(instance, location, evaluation_path, ctx) .collect(); ErrorIterator::from_iterator(errors.into_iter()) } @@ -250,14 +270,21 @@ impl Validate for IfThenElseValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { - let if_node = self.schema.evaluate_instance(instance, location, ctx); + let if_node = self + .schema + .evaluate_instance(instance, location, evaluation_path, ctx); if if_node.valid { - let then_node = self.then_schema.evaluate_instance(instance, location, ctx); + let then_node = + self.then_schema + .evaluate_instance(instance, location, evaluation_path, ctx); EvaluationResult::from_children(vec![if_node, then_node]) } else { - let else_node = self.else_schema.evaluate_instance(instance, location, ctx); + let else_node = + self.else_schema + .evaluate_instance(instance, location, evaluation_path, ctx); EvaluationResult::from_children(vec![else_node]) } } diff --git a/crates/jsonschema/src/keywords/items.rs b/crates/jsonschema/src/keywords/items.rs index b47d21ab..afa3a2c8 100644 --- a/crates/jsonschema/src/keywords/items.rs +++ b/crates/jsonschema/src/keywords/items.rs @@ -4,7 +4,7 @@ use crate::{ evaluation::Annotations, keywords::CompilationResult, node::SchemaNode, - paths::LazyLocation, + paths::{LazyLocation, LazyRefPath}, validator::{EvaluationResult, Validate, ValidationContext}, ValidationError, }; @@ -47,11 +47,12 @@ impl Validate for ItemsArrayValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Array(items) = instance { for (idx, (item, node)) in items.iter().zip(self.items.iter()).enumerate() { - node.validate(item, &location.push(idx), ctx)?; + node.validate(item, &location.push(idx), evaluation_path, ctx)?; } } Ok(()) @@ -61,12 +62,13 @@ impl Validate for ItemsArrayValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Array(items) = instance { let mut errors = Vec::new(); for (idx, (item, node)) in items.iter().zip(self.items.iter()).enumerate() { - errors.extend(node.iter_errors(item, &location.push(idx), ctx)); + errors.extend(node.iter_errors(item, &location.push(idx), evaluation_path, ctx)); } ErrorIterator::from_iterator(errors.into_iter()) } else { @@ -78,12 +80,18 @@ impl Validate for ItemsArrayValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Array(items) = instance { let mut children = Vec::with_capacity(self.items.len().min(items.len())); for (idx, (item, node)) in items.iter().zip(self.items.iter()).enumerate() { - children.push(node.evaluate_instance(item, &location.push(idx), ctx)); + children.push(node.evaluate_instance( + item, + &location.push(idx), + evaluation_path, + ctx, + )); } EvaluationResult::from_children(children) } else { @@ -117,11 +125,13 @@ impl Validate for ItemsObjectValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Array(items) = instance { for (idx, item) in items.iter().enumerate() { - self.node.validate(item, &location.push(idx), ctx)?; + self.node + .validate(item, &location.push(idx), evaluation_path, ctx)?; } } Ok(()) @@ -131,12 +141,18 @@ impl Validate for ItemsObjectValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Array(items) = instance { let mut errors = Vec::new(); for (idx, item) in items.iter().enumerate() { - errors.extend(self.node.iter_errors(item, &location.push(idx), ctx)); + errors.extend(self.node.iter_errors( + item, + &location.push(idx), + evaluation_path, + ctx, + )); } ErrorIterator::from_iterator(errors.into_iter()) } else { @@ -148,12 +164,18 @@ impl Validate for ItemsObjectValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Array(items) = instance { let mut children = Vec::with_capacity(items.len()); for (idx, item) in items.iter().enumerate() { - children.push(self.node.evaluate_instance(item, &location.push(idx), ctx)); + children.push(self.node.evaluate_instance( + item, + &location.push(idx), + evaluation_path, + ctx, + )); } let schema_was_applied = !items.is_empty(); let mut result = EvaluationResult::from_children(children); @@ -202,12 +224,17 @@ impl Validate for ItemsObjectSkipPrefixValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Array(items) = instance { for (idx, item) in items.iter().skip(self.skip_prefix).enumerate() { - self.node - .validate(item, &location.push(idx + self.skip_prefix), ctx)?; + self.node.validate( + item, + &location.push(idx + self.skip_prefix), + evaluation_path, + ctx, + )?; } } Ok(()) @@ -217,6 +244,7 @@ impl Validate for ItemsObjectSkipPrefixValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Array(items) = instance { @@ -225,6 +253,7 @@ impl Validate for ItemsObjectSkipPrefixValidator { errors.extend(self.node.iter_errors( item, &location.push(idx + self.skip_prefix), + evaluation_path, ctx, )); } @@ -238,12 +267,18 @@ impl Validate for ItemsObjectSkipPrefixValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Array(items) = instance { let mut children = Vec::with_capacity(items.len().saturating_sub(self.skip_prefix)); for (idx, item) in items.iter().enumerate().skip(self.skip_prefix) { - children.push(self.node.evaluate_instance(item, &location.push(idx), ctx)); + children.push(self.node.evaluate_instance( + item, + &location.push(idx), + evaluation_path, + ctx, + )); } let schema_was_applied = items.len() > self.skip_prefix; let mut result = EvaluationResult::from_children(children); diff --git a/crates/jsonschema/src/keywords/legacy/type_draft_4.rs b/crates/jsonschema/src/keywords/legacy/type_draft_4.rs index a88d40bb..205f01b9 100644 --- a/crates/jsonschema/src/keywords/legacy/type_draft_4.rs +++ b/crates/jsonschema/src/keywords/legacy/type_draft_4.rs @@ -2,9 +2,9 @@ use crate::{ compiler, error::ValidationError, keywords::{type_, CompilationResult}, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, types::{JsonType, JsonTypeSet}, - validator::{Validate, ValidationContext}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{json, Map, Number, Value}; use std::str::FromStr; @@ -25,8 +25,9 @@ impl MultipleTypesValidator { types = types.insert(ty); } else { return Err(ValidationError::enumeration( - Location::new(), + location.clone(), location, + Location::new(), item, &json!([ "array", "boolean", "integer", "null", "number", "object", "string" @@ -36,8 +37,9 @@ impl MultipleTypesValidator { } _ => { return Err(ValidationError::single_type_error( - Location::new(), + location.clone(), location, + Location::new(), item, JsonType::String, )) @@ -56,6 +58,7 @@ impl Validate for MultipleTypesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -63,6 +66,7 @@ impl Validate for MultipleTypesValidator { } else { Err(ValidationError::multiple_type_error( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.types, @@ -94,18 +98,21 @@ impl Validate for IntegerTypeValidator { &self, instance: &'i Value, location: &LazyLocation, - ctx: &mut ValidationContext, + evaluation_path: &LazyRefPath, + _ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { - if self.is_valid(instance, ctx) { - Ok(()) - } else { - Err(ValidationError::single_type_error( - self.location.clone(), - location.into(), - instance, - JsonType::Integer, - )) + if let Value::Number(num) = instance { + if is_integer(num) { + return Ok(()); + } } + Err(ValidationError::single_type_error( + self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), + location.into(), + instance, + JsonType::Integer, + )) } } @@ -129,8 +136,9 @@ pub(crate) fn compile<'a>( Some(compile_single_type(ty.as_str(), location, item)) } else { Some(Err(ValidationError::single_type_error( - Location::new(), + location.clone(), location, + Location::new(), item, JsonType::String, ))) @@ -139,14 +147,18 @@ pub(crate) fn compile<'a>( Some(MultipleTypesValidator::compile(items, location)) } } - _ => Some(Err(ValidationError::multiple_type_error( - Location::new(), - ctx.location().clone(), - schema, - JsonTypeSet::empty() - .insert(JsonType::String) - .insert(JsonType::Array), - ))), + _ => { + let location = ctx.location().join("type"); + Some(Err(ValidationError::multiple_type_error( + location.clone(), + location, + Location::new(), + schema, + JsonTypeSet::empty() + .insert(JsonType::String) + .insert(JsonType::Array), + ))) + } } } @@ -164,8 +176,9 @@ fn compile_single_type<'a>( Ok(JsonType::Object) => type_::ObjectTypeValidator::compile(location), Ok(JsonType::String) => type_::StringTypeValidator::compile(location), Err(()) => Err(ValidationError::custom( - Location::new(), + location.clone(), location, + Location::new(), instance, "Unexpected type", )), diff --git a/crates/jsonschema/src/keywords/max_items.rs b/crates/jsonschema/src/keywords/max_items.rs index 73990962..d987fa21 100644 --- a/crates/jsonschema/src/keywords/max_items.rs +++ b/crates/jsonschema/src/keywords/max_items.rs @@ -4,8 +4,8 @@ use crate::{ compiler, error::ValidationError, keywords::{helpers::fail_on_non_positive_integer, CompilationResult}, - paths::{LazyLocation, Location}, - validator::{Validate, ValidationContext}, + paths::{LazyLocation, LazyRefPath, Location}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -54,12 +54,14 @@ impl Validate for MaxItemsValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, _ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Array(items) = instance { if (items.len() as u64) > self.limit { return Err(ValidationError::max_items( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.limit, diff --git a/crates/jsonschema/src/keywords/max_length.rs b/crates/jsonschema/src/keywords/max_length.rs index dde2ca27..617881b7 100644 --- a/crates/jsonschema/src/keywords/max_length.rs +++ b/crates/jsonschema/src/keywords/max_length.rs @@ -4,8 +4,8 @@ use crate::{ compiler, error::ValidationError, keywords::{helpers::fail_on_non_positive_integer, CompilationResult}, - paths::{LazyLocation, Location}, - validator::{Validate, ValidationContext}, + paths::{LazyLocation, LazyRefPath, Location}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -54,12 +54,14 @@ impl Validate for MaxLengthValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, _ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::String(item) = instance { if (bytecount::num_chars(item.as_bytes()) as u64) > self.limit { return Err(ValidationError::max_length( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.limit, diff --git a/crates/jsonschema/src/keywords/max_properties.rs b/crates/jsonschema/src/keywords/max_properties.rs index c6c6e1d8..a5307c04 100644 --- a/crates/jsonschema/src/keywords/max_properties.rs +++ b/crates/jsonschema/src/keywords/max_properties.rs @@ -4,8 +4,8 @@ use crate::{ compiler, error::ValidationError, keywords::{helpers::fail_on_non_positive_integer, CompilationResult}, - paths::{LazyLocation, Location}, - validator::{Validate, ValidationContext}, + paths::{LazyLocation, LazyRefPath, Location}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -54,12 +54,14 @@ impl Validate for MaxPropertiesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, _ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { if (item.len() as u64) > self.limit { return Err(ValidationError::max_properties( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.limit, diff --git a/crates/jsonschema/src/keywords/min_items.rs b/crates/jsonschema/src/keywords/min_items.rs index 4e4b1b32..abeda187 100644 --- a/crates/jsonschema/src/keywords/min_items.rs +++ b/crates/jsonschema/src/keywords/min_items.rs @@ -4,8 +4,8 @@ use crate::{ compiler, error::ValidationError, keywords::{helpers::fail_on_non_positive_integer, CompilationResult}, - paths::{LazyLocation, Location}, - validator::{Validate, ValidationContext}, + paths::{LazyLocation, LazyRefPath, Location}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -54,12 +54,14 @@ impl Validate for MinItemsValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, _ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Array(items) = instance { if (items.len() as u64) < self.limit { return Err(ValidationError::min_items( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.limit, diff --git a/crates/jsonschema/src/keywords/min_length.rs b/crates/jsonschema/src/keywords/min_length.rs index 97a98eba..abb3012c 100644 --- a/crates/jsonschema/src/keywords/min_length.rs +++ b/crates/jsonschema/src/keywords/min_length.rs @@ -4,8 +4,8 @@ use crate::{ compiler, error::ValidationError, keywords::{helpers::fail_on_non_positive_integer, CompilationResult}, - paths::{LazyLocation, Location}, - validator::{Validate, ValidationContext}, + paths::{LazyLocation, LazyRefPath, Location}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -54,12 +54,14 @@ impl Validate for MinLengthValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, _ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::String(item) = instance { if (bytecount::num_chars(item.as_bytes()) as u64) < self.limit { return Err(ValidationError::min_length( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.limit, diff --git a/crates/jsonschema/src/keywords/min_properties.rs b/crates/jsonschema/src/keywords/min_properties.rs index 9841ca5d..a60a0b2b 100644 --- a/crates/jsonschema/src/keywords/min_properties.rs +++ b/crates/jsonschema/src/keywords/min_properties.rs @@ -4,8 +4,8 @@ use crate::{ compiler, error::ValidationError, keywords::{helpers::fail_on_non_positive_integer, CompilationResult}, - paths::{LazyLocation, Location}, - validator::{Validate, ValidationContext}, + paths::{LazyLocation, LazyRefPath, Location}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -54,12 +54,14 @@ impl Validate for MinPropertiesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, _ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { if (item.len() as u64) < self.limit { return Err(ValidationError::min_properties( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.limit, diff --git a/crates/jsonschema/src/keywords/minmax.rs b/crates/jsonschema/src/keywords/minmax.rs index ea5c005a..5d3c1a2b 100644 --- a/crates/jsonschema/src/keywords/minmax.rs +++ b/crates/jsonschema/src/keywords/minmax.rs @@ -3,10 +3,10 @@ use crate::{ error::ValidationError, ext::numeric, keywords::CompilationResult, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, thread::ThreadBound, types::JsonType, - validator::{Validate, ValidationContext}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use num_cmp::NumCmp; use serde_json::{Map, Value}; @@ -38,6 +38,7 @@ macro_rules! define_numeric_keywords { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -45,6 +46,7 @@ macro_rules! define_numeric_keywords { } else { Err(ValidationError::$error_fn_name( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.limit_val.clone(), @@ -52,7 +54,7 @@ macro_rules! define_numeric_keywords { } } - fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool { + fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool { if let Value::Number(item) = instance { $fn_name(item, self.limit) } else { @@ -74,7 +76,8 @@ define_numeric_keywords!( #[cfg(feature = "arbitrary-precision")] pub(crate) mod bigint_validators { use super::{ - numeric, LazyLocation, Location, Validate, ValidationContext, ValidationError, Value, + capture_evaluation_path, numeric, LazyLocation, LazyRefPath, Location, Validate, + ValidationContext, ValidationError, Value, }; use crate::ext::numeric::bignum::{ f64_ge_bigfrac, f64_ge_bigint, f64_gt_bigfrac, f64_gt_bigint, f64_le_bigfrac, @@ -105,6 +108,7 @@ pub(crate) mod bigint_validators { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -112,6 +116,7 @@ pub(crate) mod bigint_validators { } else { Err(ValidationError::$error_fn( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.limit_val.clone(), @@ -119,7 +124,7 @@ pub(crate) mod bigint_validators { } } - fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool { + fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool { use fraction::BigFraction; if let Value::Number(item) = instance { // Try to parse instance as BigInt first @@ -216,6 +221,7 @@ pub(crate) mod bigint_validators { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -223,6 +229,7 @@ pub(crate) mod bigint_validators { } else { Err(ValidationError::$error_fn( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.limit_val.clone(), @@ -230,7 +237,7 @@ pub(crate) mod bigint_validators { } } - fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool { + fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool { if let Value::Number(item) = instance { // Try to parse instance as BigFraction for exact precision if let Some(instance_bigfrac) = try_parse_bigfraction(item) { @@ -305,10 +312,16 @@ where Ok(Box::new(V::from((limit, schema.clone(), location)))) } -fn number_type_error<'a>(ctx: &compiler::Context, schema: &'a Value) -> CompilationResult<'a> { +fn number_type_error<'a>( + ctx: &compiler::Context, + keyword: &str, + schema: &'a Value, +) -> CompilationResult<'a> { + let location = ctx.location().join(keyword); Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::Number, )) @@ -430,7 +443,7 @@ pub(crate) fn compile_minimum<'a>( ) -> Option> { match schema { Value::Number(limit) => create_numeric_validator!(Minimum, ctx, "minimum", limit, schema), - _ => Some(number_type_error(ctx, schema)), + _ => Some(number_type_error(ctx, "minimum", schema)), } } @@ -442,7 +455,7 @@ pub(crate) fn compile_maximum<'a>( ) -> Option> { match schema { Value::Number(limit) => create_numeric_validator!(Maximum, ctx, "maximum", limit, schema), - _ => Some(number_type_error(ctx, schema)), + _ => Some(number_type_error(ctx, "maximum", schema)), } } @@ -456,7 +469,7 @@ pub(crate) fn compile_exclusive_minimum<'a>( Value::Number(limit) => { create_numeric_validator!(ExclusiveMinimum, ctx, "exclusiveMinimum", limit, schema) } - _ => Some(number_type_error(ctx, schema)), + _ => Some(number_type_error(ctx, "exclusiveMinimum", schema)), } } @@ -470,7 +483,7 @@ pub(crate) fn compile_exclusive_maximum<'a>( Value::Number(limit) => { create_numeric_validator!(ExclusiveMaximum, ctx, "exclusiveMaximum", limit, schema) } - _ => Some(number_type_error(ctx, schema)), + _ => Some(number_type_error(ctx, "exclusiveMaximum", schema)), } } diff --git a/crates/jsonschema/src/keywords/multiple_of.rs b/crates/jsonschema/src/keywords/multiple_of.rs index bcb75d17..b46b4db2 100644 --- a/crates/jsonschema/src/keywords/multiple_of.rs +++ b/crates/jsonschema/src/keywords/multiple_of.rs @@ -3,9 +3,9 @@ use crate::{ error::ValidationError, ext::numeric, keywords::CompilationResult, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, types::JsonType, - validator::{Validate, ValidationContext}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -45,6 +45,7 @@ impl Validate for MultipleOfFloatValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if !self.is_valid(instance, ctx) { @@ -52,6 +53,7 @@ impl Validate for MultipleOfFloatValidator { { return Err(ValidationError::multiple_of( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, Value::Number(self.original_value.clone()), @@ -61,6 +63,7 @@ impl Validate for MultipleOfFloatValidator { { return Err(ValidationError::multiple_of( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.multiple_of, @@ -107,6 +110,7 @@ impl Validate for MultipleOfIntegerValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if !self.is_valid(instance, ctx) { @@ -114,6 +118,7 @@ impl Validate for MultipleOfIntegerValidator { { return Err(ValidationError::multiple_of( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, Value::Number(self.original_value.clone()), @@ -123,6 +128,7 @@ impl Validate for MultipleOfIntegerValidator { { return Err(ValidationError::multiple_of( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.multiple_of, @@ -216,11 +222,13 @@ impl Validate for MultipleOfBigIntValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if !self.is_valid(instance, ctx) { return Err(ValidationError::multiple_of( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, Value::Number(self.original_value.clone()), @@ -284,11 +292,13 @@ impl Validate for MultipleOfBigFracValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if !self.is_valid(instance, ctx) { return Err(ValidationError::multiple_of( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, Value::Number(self.original_value.clone()), @@ -360,9 +370,11 @@ pub(crate) fn compile<'a>( None } } else { + let location = ctx.location().join("multipleOf"); Some(Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::Number, ))) diff --git a/crates/jsonschema/src/keywords/not.rs b/crates/jsonschema/src/keywords/not.rs index ed0a2891..af798e36 100644 --- a/crates/jsonschema/src/keywords/not.rs +++ b/crates/jsonschema/src/keywords/not.rs @@ -3,8 +3,8 @@ use crate::{ error::ValidationError, keywords::CompilationResult, node::SchemaNode, - paths::LazyLocation, - validator::{Validate, ValidationContext}, + paths::{LazyLocation, LazyRefPath}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -34,6 +34,7 @@ impl Validate for NotValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -41,6 +42,7 @@ impl Validate for NotValidator { } else { Err(ValidationError::not( self.node.location().clone(), + capture_evaluation_path(self.node.location(), evaluation_path), location.into(), instance, self.original.clone(), diff --git a/crates/jsonschema/src/keywords/one_of.rs b/crates/jsonschema/src/keywords/one_of.rs index cd5c31a3..ac8957e2 100644 --- a/crates/jsonschema/src/keywords/one_of.rs +++ b/crates/jsonschema/src/keywords/one_of.rs @@ -4,9 +4,9 @@ use crate::{ evaluation::ErrorDescription, keywords::CompilationResult, node::SchemaNode, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, types::JsonType, - validator::{EvaluationResult, Validate, ValidationContext}, + validator::{capture_evaluation_path, EvaluationResult, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -31,9 +31,11 @@ impl OneOfValidator { location: ctx.location().clone(), })) } else { + let location = ctx.location().join("oneOf"); Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::Array, )) @@ -70,6 +72,7 @@ impl Validate for OneOfValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { let first_valid_idx = self.get_first_valid(instance, ctx); @@ -77,11 +80,16 @@ impl Validate for OneOfValidator { if self.are_others_valid(instance, idx, ctx) { return Err(ValidationError::one_of_multiple_valid( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.schemas .iter() - .map(|schema| schema.iter_errors(instance, location, ctx).collect()) + .map(|schema| { + schema + .iter_errors(instance, location, evaluation_path, ctx) + .collect() + }) .collect(), )); } @@ -89,11 +97,16 @@ impl Validate for OneOfValidator { } else { Err(ValidationError::one_of_not_valid( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.schemas .iter() - .map(|schema| schema.iter_errors(instance, location, ctx).collect()) + .map(|schema| { + schema + .iter_errors(instance, location, evaluation_path, ctx) + .collect() + }) .collect(), )) } @@ -103,40 +116,70 @@ impl Validate for OneOfValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { - let total = self.schemas.len(); - let mut failures = Vec::with_capacity(total); - let mut iter = self.schemas.iter(); - while let Some(node) = iter.next() { - let child = node.evaluate_instance(instance, location, ctx); - if child.valid { - let mut successes = Vec::with_capacity(total.saturating_sub(failures.len())); - successes.push(child); - for node in iter { - let next = node.evaluate_instance(instance, location, ctx); - if next.valid { - successes.push(next); - } + // In most cases it is much faster to use cheap `is_valid` that does not build evaluation path and then + // re-run slower `evaluate` on a subset. It assumes that validation more often succeeds + // than not in real use cases. + let mut valid_indices: Vec = Vec::new(); + for (idx, node) in self.schemas.iter().enumerate() { + if node.is_valid(instance, ctx) { + valid_indices.push(idx); + // Early exit if we find more than one valid - we know it's invalid + if valid_indices.len() > 1 { + break; } - return match successes.len() { - 1 => EvaluationResult::from(successes.remove(0)), - _ => EvaluationResult::Invalid { - errors: vec![ErrorDescription::new( - "oneOf", - "more than one subschema succeeded".to_string(), - )], - children: successes, - annotations: None, - }, - }; } - failures.push(child); } - EvaluationResult::Invalid { - errors: Vec::new(), - children: failures, - annotations: None, + + match valid_indices.len() { + 0 => { + // No valid schemas - need to evaluate all for error details + let failures: Vec<_> = self + .schemas + .iter() + .map(|node| node.evaluate_instance(instance, location, evaluation_path, ctx)) + .collect(); + EvaluationResult::Invalid { + errors: Vec::new(), + children: failures, + annotations: None, + } + } + 1 => { + // Exactly one valid - only evaluate that one for output + let child = self.schemas[valid_indices[0]].evaluate_instance( + instance, + location, + evaluation_path, + ctx, + ); + EvaluationResult::from(child) + } + _ => { + // Multiple valid - evaluate all valid ones for error output + // We already found at least 2, but there may be more + let mut successes = Vec::with_capacity(valid_indices.len()); + for (idx, node) in self.schemas.iter().enumerate() { + // Check indices we already know, or test remaining schemas + if valid_indices.contains(&idx) || node.is_valid(instance, ctx) { + let child = + node.evaluate_instance(instance, location, evaluation_path, ctx); + if child.valid { + successes.push(child); + } + } + } + EvaluationResult::Invalid { + errors: vec![ErrorDescription::new( + "oneOf", + "more than one subschema succeeded".to_string(), + )], + children: successes, + annotations: None, + } + } } } } diff --git a/crates/jsonschema/src/keywords/pattern.rs b/crates/jsonschema/src/keywords/pattern.rs index 5b730310..3d70038c 100644 --- a/crates/jsonschema/src/keywords/pattern.rs +++ b/crates/jsonschema/src/keywords/pattern.rs @@ -5,10 +5,10 @@ use crate::{ error::ValidationError, keywords::CompilationResult, options::PatternEngineOptions, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, regex::{RegexEngine, RegexError}, types::JsonType, - validator::{Validate, ValidationContext}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -22,6 +22,7 @@ impl Validate for PatternValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, _ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::String(item) = instance { @@ -30,6 +31,7 @@ impl Validate for PatternValidator { if !is_match { return Err(ValidationError::pattern( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.regex.pattern().to_string(), @@ -39,6 +41,7 @@ impl Validate for PatternValidator { Err(e) => { return Err(ValidationError::backtrack_limit( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, e.into_backtrack_error() @@ -64,8 +67,8 @@ pub(crate) fn compile<'a>( _: &'a Map, schema: &'a Value, ) -> Option> { - match schema { - Value::String(item) => match ctx.config().pattern_options() { + if let Value::String(item) = schema { + match ctx.config().pattern_options() { PatternEngineOptions::FancyRegex { .. } => { let Ok(regex) = ctx.get_or_compile_regex(item) else { return Some(Err(invalid_regex(ctx, schema))); @@ -84,18 +87,22 @@ pub(crate) fn compile<'a>( location: ctx.location().join("pattern"), }))) } - }, - _ => Some(Err(ValidationError::single_type_error( + } + } else { + let location = ctx.location().join("pattern"); + Some(Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::String, - ))), + ))) } } fn invalid_regex<'a>(ctx: &compiler::Context, schema: &'a Value) -> ValidationError<'a> { - ValidationError::format(Location::new(), ctx.location().clone(), schema, "regex") + let location = ctx.location().join("pattern"); + ValidationError::format(location.clone(), location, Location::new(), schema, "regex") } #[cfg(test)] diff --git a/crates/jsonschema/src/keywords/pattern_properties.rs b/crates/jsonschema/src/keywords/pattern_properties.rs index 6b799b07..40fe1d80 100644 --- a/crates/jsonschema/src/keywords/pattern_properties.rs +++ b/crates/jsonschema/src/keywords/pattern_properties.rs @@ -7,7 +7,7 @@ use crate::{ keywords::CompilationResult, node::SchemaNode, options::PatternEngineOptions, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, regex::RegexEngine, types::JsonType, validator::{EvaluationResult, Validate, ValidationContext}, @@ -38,13 +38,14 @@ impl Validate for PatternPropertiesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { for (key, value) in item { for (re, node) in &self.patterns { if re.is_match(key).unwrap_or(false) { - node.validate(value, &location.push(key), ctx)?; + node.validate(value, &location.push(key), evaluation_path, ctx)?; } } } @@ -56,6 +57,7 @@ impl Validate for PatternPropertiesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = instance { @@ -63,7 +65,12 @@ impl Validate for PatternPropertiesValidator { for (re, node) in &self.patterns { for (key, value) in item { if re.is_match(key).unwrap_or(false) { - errors.extend(node.iter_errors(value, &location.push(key.as_str()), ctx)); + errors.extend(node.iter_errors( + value, + &location.push(key.as_str()), + evaluation_path, + ctx, + )); } } } @@ -77,6 +84,7 @@ impl Validate for PatternPropertiesValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(item) = instance { @@ -89,6 +97,7 @@ impl Validate for PatternPropertiesValidator { children.push(node.evaluate_instance( value, &location.push(key.as_str()), + evaluation_path, ctx, )); } @@ -126,12 +135,14 @@ impl Validate for SingleValuePatternPropertiesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { for (key, value) in item { if self.regex.is_match(key).unwrap_or(false) { - self.node.validate(value, &location.push(key), ctx)?; + self.node + .validate(value, &location.push(key), evaluation_path, ctx)?; } } } @@ -142,16 +153,19 @@ impl Validate for SingleValuePatternPropertiesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = instance { let mut errors = Vec::new(); for (key, value) in item { if self.regex.is_match(key).unwrap_or(false) { - errors.extend( - self.node - .iter_errors(value, &location.push(key.as_str()), ctx), - ); + errors.extend(self.node.iter_errors( + value, + &location.push(key.as_str()), + evaluation_path, + ctx, + )); } } ErrorIterator::from_iterator(errors.into_iter()) @@ -164,6 +178,7 @@ impl Validate for SingleValuePatternPropertiesValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(item) = instance { @@ -175,6 +190,7 @@ impl Validate for SingleValuePatternPropertiesValidator { children.push(self.node.evaluate_instance( value, &location.push(key.as_str()), + evaluation_path, ctx, )); } @@ -203,9 +219,11 @@ pub(crate) fn compile<'a>( } let Value::Object(map) = schema else { + let location = ctx.location().join("patternProperties"); return Some(Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::Object, ))); @@ -241,7 +259,8 @@ pub(crate) fn compile<'a>( } fn invalid_regex<'a>(ctx: &compiler::Context, schema: &'a Value) -> ValidationError<'a> { - ValidationError::format(Location::new(), ctx.location().clone(), schema, "regex") + let location = ctx.location().clone(); + ValidationError::format(location.clone(), location, Location::new(), schema, "regex") } /// Compile every `(pattern, subschema)` pair into `(regex, node)` tuples. diff --git a/crates/jsonschema/src/keywords/prefix_items.rs b/crates/jsonschema/src/keywords/prefix_items.rs index b46c2d33..99dab329 100644 --- a/crates/jsonschema/src/keywords/prefix_items.rs +++ b/crates/jsonschema/src/keywords/prefix_items.rs @@ -3,7 +3,7 @@ use crate::{ error::{no_error, ErrorIterator, ValidationError}, evaluation::Annotations, node::SchemaNode, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, types::JsonType, validator::{EvaluationResult, Validate, ValidationContext}, }; @@ -50,11 +50,12 @@ impl Validate for PrefixItemsValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Array(items) = instance { for (idx, (schema, item)) in self.schemas.iter().zip(items.iter()).enumerate() { - schema.validate(item, &location.push(idx), ctx)?; + schema.validate(item, &location.push(idx), evaluation_path, ctx)?; } } Ok(()) @@ -64,12 +65,13 @@ impl Validate for PrefixItemsValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Array(items) = instance { let mut errors = Vec::new(); for (idx, (schema, item)) in self.schemas.iter().zip(items.iter()).enumerate() { - errors.extend(schema.iter_errors(item, &location.push(idx), ctx)); + errors.extend(schema.iter_errors(item, &location.push(idx), evaluation_path, ctx)); } ErrorIterator::from_iterator(errors.into_iter()) } else { @@ -81,6 +83,7 @@ impl Validate for PrefixItemsValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Array(items) = instance { @@ -89,7 +92,12 @@ impl Validate for PrefixItemsValidator { let mut max_index_applied = 0usize; for (idx, (schema_node, item)) in self.schemas.iter().zip(items.iter()).enumerate() { - children.push(schema_node.evaluate_instance(item, &location.push(idx), ctx)); + children.push(schema_node.evaluate_instance( + item, + &location.push(idx), + evaluation_path, + ctx, + )); max_index_applied = idx; } let annotation = if children.len() == items.len() { @@ -115,9 +123,11 @@ pub(crate) fn compile<'a>( if let Value::Array(items) = schema { Some(PrefixItemsValidator::compile(ctx, items)) } else { + let location = ctx.location().join("prefixItems"); Some(Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::Array, ))) diff --git a/crates/jsonschema/src/keywords/properties.rs b/crates/jsonschema/src/keywords/properties.rs index 284813da..4b7a81ad 100644 --- a/crates/jsonschema/src/keywords/properties.rs +++ b/crates/jsonschema/src/keywords/properties.rs @@ -4,7 +4,7 @@ use crate::{ evaluation::Annotations, keywords::CompilationResult, node::SchemaNode, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, types::JsonType, validator::{EvaluationResult, Validate, ValidationContext}, }; @@ -17,25 +17,26 @@ pub(crate) struct PropertiesValidator { impl PropertiesValidator { #[inline] pub(crate) fn compile<'a>(ctx: &compiler::Context, schema: &'a Value) -> CompilationResult<'a> { - match schema { - Value::Object(map) => { - let ctx = ctx.new_at_location("properties"); - let mut properties = Vec::with_capacity(map.len()); - for (key, subschema) in map { - let ctx = ctx.new_at_location(key.as_str()); - properties.push(( - key.clone(), - compiler::compile(&ctx, ctx.as_resource_ref(subschema))?, - )); - } - Ok(Box::new(PropertiesValidator { properties })) + if let Value::Object(map) = schema { + let ctx = ctx.new_at_location("properties"); + let mut properties = Vec::with_capacity(map.len()); + for (key, subschema) in map { + let ctx = ctx.new_at_location(key.as_str()); + properties.push(( + key.clone(), + compiler::compile(&ctx, ctx.as_resource_ref(subschema))?, + )); } - _ => Err(ValidationError::single_type_error( + Ok(Box::new(PropertiesValidator { properties })) + } else { + let location = ctx.location().join("properties"); + Err(ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::Object, - )), + )) } } } @@ -60,12 +61,13 @@ impl Validate for PropertiesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { for (name, node) in &self.properties { if let Some(item) = item.get(name) { - node.validate(item, &location.push(name), ctx)?; + node.validate(item, &location.push(name), evaluation_path, ctx)?; } } } @@ -77,6 +79,7 @@ impl Validate for PropertiesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = instance { @@ -84,7 +87,7 @@ impl Validate for PropertiesValidator { for (name, node) in &self.properties { if let Some(prop) = item.get(name) { let instance_path = location.push(name.as_str()); - errors.extend(node.iter_errors(prop, &instance_path, ctx)); + errors.extend(node.iter_errors(prop, &instance_path, evaluation_path, ctx)); } } ErrorIterator::from_iterator(errors.into_iter()) @@ -97,6 +100,7 @@ impl Validate for PropertiesValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(props) = instance { @@ -106,7 +110,7 @@ impl Validate for PropertiesValidator { if let Some(prop) = props.get(prop_name) { let path = location.push(prop_name.as_str()); matched_props.push(prop_name.clone()); - children.push(node.evaluate_instance(prop, &path, ctx)); + children.push(node.evaluate_instance(prop, &path, evaluation_path, ctx)); } } let mut application = EvaluationResult::from_children(children); diff --git a/crates/jsonschema/src/keywords/property_names.rs b/crates/jsonschema/src/keywords/property_names.rs index 46a16d1e..29fd5e4a 100644 --- a/crates/jsonschema/src/keywords/property_names.rs +++ b/crates/jsonschema/src/keywords/property_names.rs @@ -3,8 +3,8 @@ use crate::{ error::{no_error, ErrorIterator, ValidationError}, keywords::CompilationResult, node::SchemaNode, - paths::{LazyLocation, Location}, - validator::{EvaluationResult, Validate, ValidationContext}, + paths::{LazyLocation, LazyRefPath, Location}, + validator::{capture_evaluation_path, EvaluationResult, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -38,20 +38,23 @@ impl Validate for PropertyNamesObjectValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = &instance { for key in item.keys() { let wrapper = Value::String(key.clone()); - match self.node.validate(&wrapper, location, ctx) { + match self.node.validate(&wrapper, location, evaluation_path, ctx) { Ok(()) => {} Err(error) => { + let schema_path = error.schema_path().clone(); return Err(ValidationError::property_names( - error.schema_path().clone(), + schema_path.clone(), + capture_evaluation_path(&schema_path, evaluation_path), location.into(), instance, error.to_owned(), - )) + )); } } } @@ -63,20 +66,26 @@ impl Validate for PropertyNamesObjectValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = &instance { let mut errors = Vec::new(); for key in item.keys() { let wrapper = Value::String(key.clone()); - errors.extend(self.node.iter_errors(&wrapper, location, ctx).map(|error| { - ValidationError::property_names( - error.schema_path().clone(), + for error in self + .node + .iter_errors(&wrapper, location, evaluation_path, ctx) + { + let schema_path = error.schema_path().clone(); + errors.push(ValidationError::property_names( + schema_path.clone(), + capture_evaluation_path(&schema_path, evaluation_path), location.into(), instance, error.to_owned(), - ) - })); + )); + } } ErrorIterator::from_iterator(errors.into_iter()) } else { @@ -88,13 +97,19 @@ impl Validate for PropertyNamesObjectValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(item) = instance { let mut children = Vec::with_capacity(item.len()); for key in item.keys() { let wrapper = Value::String(key.clone()); - children.push(self.node.evaluate_instance(&wrapper, location, ctx)); + children.push(self.node.evaluate_instance( + &wrapper, + location, + evaluation_path, + ctx, + )); } EvaluationResult::from_children(children) } else { @@ -129,6 +144,7 @@ impl Validate for PropertyNamesBooleanValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -136,6 +152,7 @@ impl Validate for PropertyNamesBooleanValidator { } else { Err(ValidationError::false_schema( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, )) diff --git a/crates/jsonschema/src/keywords/ref_.rs b/crates/jsonschema/src/keywords/ref_.rs index deb64e4c..db4e1538 100644 --- a/crates/jsonschema/src/keywords/ref_.rs +++ b/crates/jsonschema/src/keywords/ref_.rs @@ -1,9 +1,87 @@ use crate::{ - compiler, keywords::CompilationResult, paths::Location, types::JsonType, validator::Validate, + compiler, + error::ErrorIterator, + keywords::CompilationResult, + paths::{LazyLocation, LazyRefPath, Location}, + types::JsonType, + validator::{EvaluationResult, Validate, ValidationContext}, ValidationError, }; use serde_json::{Map, Value}; +/// Tracks `$ref` traversals for `evaluation_path` (JSON Schema 2020-12 Core, Section 12.4.2). +/// +/// Pushes the `$ref` location onto the context's path stack before delegating +/// to the inner validator, then pops it when done. +struct RefValidator { + inner: Box, + /// E.g., "/properties/foo/$ref" + ref_location: Location, + /// E.g., "/$defs/item" + ref_target_base: Location, +} + +impl Validate for RefValidator { + fn is_valid(&self, instance: &Value, ctx: &mut ValidationContext) -> bool { + self.inner.is_valid(instance, ctx) + } + + fn validate<'i>( + &self, + instance: &'i Value, + location: &LazyLocation, + evaluation_path: &LazyRefPath, + ctx: &mut ValidationContext, + ) -> Result<(), ValidationError<'i>> { + let child_path = evaluation_path.push(&self.ref_location, &self.ref_target_base); + self.inner.validate(instance, location, &child_path, ctx) + } + + fn iter_errors<'i>( + &self, + instance: &'i Value, + location: &LazyLocation, + evaluation_path: &LazyRefPath, + ctx: &mut ValidationContext, + ) -> ErrorIterator<'i> { + let child_path = evaluation_path.push(&self.ref_location, &self.ref_target_base); + self.inner.iter_errors(instance, location, &child_path, ctx) + } + + fn evaluate( + &self, + instance: &Value, + location: &LazyLocation, + evaluation_path: &LazyRefPath, + ctx: &mut ValidationContext, + ) -> EvaluationResult { + let child_path = evaluation_path.push(&self.ref_location, &self.ref_target_base); + self.inner.evaluate(instance, location, &child_path, ctx) + } + + /// Returns `ref_target_base` for `schema_path` output. + /// + /// Per JSON Schema 2020-12 Core Section 12.4.2, `schema_path` "MUST NOT include + /// by-reference applicators such as `$ref` or `$dynamicRef`". + fn canonical_location(&self) -> Option<&Location> { + Some(&self.ref_target_base) + } +} + +/// Extract `ref_target_base` from a resolved URI fragment. +/// +/// JSON Pointer fragments (starting with `/`) become the location path. +/// Anchor fragments (plain names like `#node`) resolve to root. +fn extract_ref_target_base(alias: &referencing::Uri) -> Location { + if let Some(fragment) = alias.fragment() { + let fragment = fragment.as_str(); + if fragment.starts_with('/') { + return Location::from_escaped(fragment); + } + } + Location::new() +} + fn compile_reference_validator<'a>( ctx: &compiler::Context, reference: &str, @@ -21,16 +99,24 @@ fn compile_reference_validator<'a>( Err(error) => return Some(Err(error)), }; + // Direct self-reference or empty string ref ("" per RFC 3986) - skip to avoid infinite recursion if alias == current_location || (reference.is_empty() && alias.strip_fragment() == current_location.strip_fragment()) { - // Direct self-reference would recurse indefinitely, treat it as an annotation-only schema. - // Empty string $ref ("") is a same-document reference per RFC 3986, equivalent to "#" return None; } + let ref_location = ctx.location().join(keyword); + let ref_target_base = extract_ref_target_base(&alias); + match ctx.lookup_maybe_recursive(reference) { - Ok(Some(validator)) => return Some(Ok(validator)), + Ok(Some(validator)) => { + return Some(Ok(Box::new(RefValidator { + inner: validator, + ref_location, + ref_target_base, + }))); + } Ok(None) => {} Err(error) => return Some(Err(error)), } @@ -45,17 +131,20 @@ fn compile_reference_validator<'a>( }; let vocabularies = ctx.registry.find_vocabularies(draft, contents); let resource_ref = draft.create_resource_ref(contents); - let ctx = ctx.with_resolver_and_draft( + let inner_ctx = ctx.with_resolver_and_draft( resolver, resource_ref.draft(), vocabularies, - ctx.location().join(keyword), + ref_target_base.clone(), ); Some( - compiler::compile_with_alias(&ctx, resource_ref, alias) + compiler::compile_with_alias(&inner_ctx, resource_ref, alias) .map(|node| { - Box::new(node.clone_with_location(ctx.location().clone(), ctx.base_uri())) - as Box + Box::new(RefValidator { + inner: Box::new(node), + ref_location, + ref_target_base, + }) as Box }) .map_err(ValidationError::to_owned), ) @@ -65,9 +154,20 @@ fn compile_recursive_validator<'a>( ctx: &compiler::Context, reference: &str, ) -> CompilationResult<'a> { - // Check if this is a circular reference first + let ref_location = ctx.location().join("$recursiveRef"); + let alias = ctx + .resolve_reference_uri(reference) + .map_err(ValidationError::from)?; + let ref_target_base = extract_ref_target_base(&alias); + match ctx.lookup_maybe_recursive(reference) { - Ok(Some(validator)) => return Ok(validator), + Ok(Some(validator)) => { + return Ok(Box::new(RefValidator { + inner: validator, + ref_location, + ref_target_base, + })); + } Ok(None) => {} Err(error) => return Err(error), } @@ -76,33 +176,39 @@ fn compile_recursive_validator<'a>( return Err(ValidationError::from(error)); } - let alias = ctx - .resolve_reference_uri(reference) - .map_err(ValidationError::from)?; let resolved = ctx .lookup_recursive_reference() .map_err(ValidationError::from)?; let (contents, resolver, draft) = resolved.into_inner(); let vocabularies = ctx.registry.find_vocabularies(draft, contents); let resource_ref = draft.create_resource_ref(contents); - let ctx = ctx.with_resolver_and_draft( + let inner_ctx = ctx.with_resolver_and_draft( resolver, resource_ref.draft(), vocabularies, - ctx.location().join("$recursiveRef"), + ref_target_base.clone(), ); - compiler::compile_with_alias(&ctx, resource_ref, alias) + compiler::compile_with_alias(&inner_ctx, resource_ref, alias) .map(|node| { - Box::new(node.clone_with_location(ctx.location().clone(), ctx.base_uri())) - as Box + Box::new(RefValidator { + inner: Box::new(node), + ref_location, + ref_target_base, + }) as Box }) .map_err(ValidationError::to_owned) } -fn invalid_reference<'a>(ctx: &compiler::Context, schema: &'a Value) -> ValidationError<'a> { +fn invalid_reference<'a>( + ctx: &compiler::Context, + keyword: &str, + schema: &'a Value, +) -> ValidationError<'a> { + let location = ctx.location().join(keyword); ValidationError::single_type_error( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, JsonType::String, ) @@ -118,7 +224,7 @@ pub(crate) fn compile_impl<'a>( if let Some(reference) = schema.as_str() { compile_reference_validator(ctx, reference, keyword) } else { - Some(Err(invalid_reference(ctx, schema))) + Some(Err(invalid_reference(ctx, keyword, schema))) } } @@ -149,7 +255,7 @@ pub(crate) fn compile_recursive_ref<'a>( Some( schema .as_str() - .ok_or_else(|| invalid_reference(ctx, schema)) + .ok_or_else(|| invalid_reference(ctx, "$recursiveRef", schema)) .and_then(|reference| compile_recursive_validator(ctx, reference)), ) } @@ -278,7 +384,8 @@ mod tests { "/properties/foo/$ref/type" )] fn location(schema: &Value, instance: &Value, expected: &str) { - tests_util::assert_schema_location(schema, instance, expected); + // For $ref tests, check evaluation_path (includes $ref traversals) + tests_util::assert_evaluation_path(schema, instance, expected); } #[test] @@ -311,18 +418,19 @@ mod tests { }); let validator = crate::validator_for(&schema).expect("Invalid schema"); let mut iter = validator.iter_errors(&instance); + // evaluation_path includes $ref traversals let expected = "/properties/things/items/properties/code/$ref/enum"; assert_eq!( iter.next() .expect("Should be present") - .schema_path() + .evaluation_path() .to_string(), expected ); assert_eq!( iter.next() .expect("Should be present") - .schema_path() + .evaluation_path() .to_string(), expected ); @@ -375,7 +483,9 @@ mod tests { vec![ ("", "/properties"), ("/foo", "/properties/foo/items"), - ("/foo/0", "/properties/foo/items/$ref/properties"), + // schemaLocation is the canonical location WITHOUT $ref (per JSON Schema spec) + // The $ref resolves to $defs/item, so properties keyword is at /$defs/item/properties + ("/foo/0", "/$defs/item/properties"), ] ; "standard $ref")] #[test_case( @@ -398,8 +508,11 @@ mod tests { }), vec![ ("", "/properties"), - ("/child", "/properties/child/$recursiveRef/properties"), - ("/child/child", "/properties/child/$recursiveRef/properties"), + // schemaLocation is the canonical location WITHOUT $recursiveRef (per JSON Schema spec) + // $recursiveRef resolves to root (where $recursiveAnchor is), so properties is at /properties + ("/child", "/properties"), + // Same for nested - still resolves to root's /properties + ("/child/child", "/properties"), ] ; "$recursiveRef")] fn keyword_locations(schema: &Value, instance: &Value, expected: Vec<(&str, &str)>) { @@ -860,4 +973,484 @@ mod tests { assert!(validator.validate(&invalid).is_err()); assert!(validator.iter_errors(&invalid).count() > 0); } + + #[test] + fn evaluation_path_through_ref() { + // Test that evaluation_path correctly includes $ref traversals + let schema = json!({ + "properties": { + "foo": {"$ref": "#/$defs/item"} + }, + "$defs": { + "item": {"type": "string"} + } + }); + let instance = json!({"foo": 42}); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + // schema_path is the canonical location (where the keyword actually is) + assert_eq!(error.schema_path().as_str(), "/$defs/item/type"); + + // evaluation_path includes the $ref traversal + assert_eq!( + error.evaluation_path().as_str(), + "/properties/foo/$ref/type" + ); + } + + #[test] + fn evaluation_path_nested_refs() { + // Test nested $ref traversals + let schema = json!({ + "$ref": "#/$defs/wrapper", + "$defs": { + "wrapper": { + "properties": { + "value": {"$ref": "#/$defs/item"} + } + }, + "item": {"type": "integer"} + } + }); + let instance = json!({"value": "not an integer"}); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + // schema_path is canonical + assert_eq!(error.schema_path().as_str(), "/$defs/item/type"); + + // evaluation_path shows full traversal through both $refs + assert_eq!( + error.evaluation_path().as_str(), + "/$ref/properties/value/$ref/type" + ); + } + + #[test] + fn evaluation_path_recursive_ref() { + // $recursiveRef should appear in evaluation path + let schema = json!({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$recursiveAnchor": true, + "type": "object", + "properties": { + "name": {"type": "string"}, + "child": {"$recursiveRef": "#"} + } + }); + let instance = json!({ + "name": "parent", + "child": { + "name": 42 + } + }); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + // schema_path is canonical (at root, since $recursiveRef resolves to root) + assert_eq!(error.schema_path().as_str(), "/properties/name/type"); + + // evaluation_path includes the $recursiveRef traversal + assert_eq!( + error.evaluation_path().as_str(), + "/properties/child/$recursiveRef/properties/name/type" + ); + } + + #[test] + fn evaluation_path_recursive_ref_deep() { + // Multiple levels of $recursiveRef + let schema = json!({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$recursiveAnchor": true, + "type": "object", + "properties": { + "value": {"type": "integer"}, + "child": {"$recursiveRef": "#"} + } + }); + let instance = json!({ + "value": 1, + "child": { + "value": 2, + "child": { + "value": "not an int" + } + } + }); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + // schema_path is canonical (at root, since $recursiveRef resolves to root) + assert_eq!(error.schema_path().as_str(), "/properties/value/type"); + + // evaluation_path shows the full traversal through both $recursiveRef + assert_eq!( + error.evaluation_path().as_str(), + "/properties/child/$recursiveRef/properties/child/$recursiveRef/properties/value/type" + ); + } + + #[test] + fn evaluation_path_dynamic_ref() { + // $dynamicRef should appear in evaluation path but NOT in schema_path + let schema = json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$dynamicAnchor": "node", + "type": "object", + "properties": { + "data": {"type": "string"}, + "child": {"$dynamicRef": "#node"} + } + }); + let instance = json!({ + "data": "parent", + "child": { + "data": 123 + } + }); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + // schema_path is the canonical location (at root, since #node anchor is at root) + assert_eq!(error.schema_path().as_str(), "/properties/data/type"); + + // evaluation_path includes the $dynamicRef traversal + assert_eq!( + error.evaluation_path().as_str(), + "/properties/child/$dynamicRef/properties/data/type" + ); + } + + #[test] + fn evaluation_path_triple_nested_ref() { + // Three levels of $ref + let schema = json!({ + "$ref": "#/$defs/level1", + "$defs": { + "level1": { + "$ref": "#/$defs/level2" + }, + "level2": { + "$ref": "#/$defs/level3" + }, + "level3": { + "type": "boolean" + } + } + }); + let instance = json!("not a boolean"); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + assert_eq!(error.schema_path().as_str(), "/$defs/level3/type"); + assert_eq!(error.evaluation_path().as_str(), "/$ref/$ref/$ref/type"); + } + + #[test] + fn evaluation_path_ref_in_allof() { + // $ref inside allOf + let schema = json!({ + "allOf": [ + {"$ref": "#/$defs/stringType"}, + {"minLength": 5} + ], + "$defs": { + "stringType": {"type": "string"} + } + }); + let instance = json!(42); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + assert_eq!(error.evaluation_path().as_str(), "/allOf/0/$ref/type"); + } + + #[test] + fn evaluation_path_ref_in_anyof() { + // $ref inside anyOf - all branches fail + let schema = json!({ + "anyOf": [ + {"$ref": "#/$defs/intType"}, + {"$ref": "#/$defs/boolType"} + ], + "$defs": { + "intType": {"type": "integer"}, + "boolType": {"type": "boolean"} + } + }); + let instance = json!("string"); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let errors: Vec<_> = validator.iter_errors(&instance).collect(); + + // anyOf produces a single error containing nested errors + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].evaluation_path().as_str(), "/anyOf"); + } + + #[test] + fn evaluation_path_ref_minimum() { + // Test minimum validator through $ref + let schema = json!({ + "properties": { + "age": {"$ref": "#/$defs/positiveInt"} + }, + "$defs": { + "positiveInt": {"type": "integer", "minimum": 0} + } + }); + let instance = json!({"age": -5}); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + assert_eq!( + error.evaluation_path().as_str(), + "/properties/age/$ref/minimum" + ); + } + + #[test] + fn evaluation_path_ref_pattern() { + // Test pattern validator through $ref + let schema = json!({ + "properties": { + "email": {"$ref": "#/$defs/emailPattern"} + }, + "$defs": { + "emailPattern": {"type": "string", "pattern": "^.+@.+$"} + } + }); + let instance = json!({"email": "not-an-email"}); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + assert_eq!( + error.evaluation_path().as_str(), + "/properties/email/$ref/pattern" + ); + } + + #[test] + fn evaluation_path_ref_required() { + // Test required validator through $ref + let schema = json!({ + "properties": { + "user": {"$ref": "#/$defs/userType"} + }, + "$defs": { + "userType": { + "type": "object", + "required": ["name"] + } + } + }); + let instance = json!({"user": {}}); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + assert_eq!( + error.evaluation_path().as_str(), + "/properties/user/$ref/required" + ); + } + + #[test] + fn evaluation_path_ref_enum() { + // Test enum validator through $ref + let schema = json!({ + "properties": { + "status": {"$ref": "#/$defs/statusEnum"} + }, + "$defs": { + "statusEnum": {"enum": ["active", "inactive"]} + } + }); + let instance = json!({"status": "unknown"}); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + assert_eq!( + error.evaluation_path().as_str(), + "/properties/status/$ref/enum" + ); + } + + #[test] + fn evaluation_path_ref_const() { + // Test const validator through $ref + let schema = json!({ + "properties": { + "version": {"$ref": "#/$defs/versionConst"} + }, + "$defs": { + "versionConst": {"const": "1.0"} + } + }); + let instance = json!({"version": "2.0"}); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + assert_eq!( + error.evaluation_path().as_str(), + "/properties/version/$ref/const" + ); + } + + #[test] + fn evaluation_path_ref_max_length() { + // Test maxLength validator through $ref + let schema = json!({ + "properties": { + "code": {"$ref": "#/$defs/shortString"} + }, + "$defs": { + "shortString": {"type": "string", "maxLength": 3} + } + }); + let instance = json!({"code": "toolong"}); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + assert_eq!( + error.evaluation_path().as_str(), + "/properties/code/$ref/maxLength" + ); + } + + #[test] + fn evaluation_path_ref_unique_items() { + // Test uniqueItems validator through $ref + let schema = json!({ + "properties": { + "tags": {"$ref": "#/$defs/uniqueArray"} + }, + "$defs": { + "uniqueArray": {"type": "array", "uniqueItems": true} + } + }); + let instance = json!({"tags": ["a", "b", "a"]}); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + assert_eq!( + error.evaluation_path().as_str(), + "/properties/tags/$ref/uniqueItems" + ); + } + + #[test] + fn evaluation_path_multiple_errors_different_refs() { + // Multiple errors through different $refs + let schema = json!({ + "properties": { + "name": {"$ref": "#/$defs/stringType"}, + "age": {"$ref": "#/$defs/intType"} + }, + "$defs": { + "stringType": {"type": "string"}, + "intType": {"type": "integer"} + } + }); + let instance = json!({"name": 123, "age": "not an int"}); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let errors: Vec<_> = validator.iter_errors(&instance).collect(); + + assert_eq!(errors.len(), 2); + + let paths: Vec<_> = errors + .iter() + .map(|e| e.evaluation_path().to_string()) + .collect(); + + assert!(paths.contains(&"/properties/name/$ref/type".to_string())); + assert!(paths.contains(&"/properties/age/$ref/type".to_string())); + } + + #[test] + fn evaluation_path_ref_with_anchor() { + // $ref using $anchor + let schema = json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "data": {"$ref": "#myAnchor"} + }, + "$defs": { + "myDef": { + "$anchor": "myAnchor", + "type": "number" + } + } + }); + let instance = json!({"data": "not a number"}); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + assert_eq!( + error.evaluation_path().as_str(), + "/properties/data/$ref/type" + ); + } + + #[test] + fn evaluation_path_items_with_ref() { + // $ref inside items + let schema = json!({ + "type": "array", + "items": {"$ref": "#/$defs/itemType"}, + "$defs": { + "itemType": {"type": "string"} + } + }); + let instance = json!([1, 2, 3]); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let errors: Vec<_> = validator.iter_errors(&instance).collect(); + + assert_eq!(errors.len(), 3); + for error in &errors { + assert_eq!(error.evaluation_path().as_str(), "/items/$ref/type"); + } + } + + #[test] + fn evaluation_path_additional_properties_with_ref() { + // additionalProperties with $ref + let schema = json!({ + "type": "object", + "additionalProperties": {"$ref": "#/$defs/valueType"}, + "$defs": { + "valueType": {"type": "integer"} + } + }); + let instance = json!({"a": "not int", "b": "also not int"}); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let errors: Vec<_> = validator.iter_errors(&instance).collect(); + + assert_eq!(errors.len(), 2); + for error in &errors { + assert_eq!( + error.evaluation_path().as_str(), + "/additionalProperties/$ref/type" + ); + } + } + + #[test] + fn schema_path_with_json_pointer_escaped_key() { + // $defs key contains special chars that need JSON Pointer escaping + let schema = json!({ + "properties": { + "data": {"$ref": "#/$defs/type~1name"} + }, + "$defs": { + "type/name": {"type": "string"} + } + }); + let instance = json!({"data": 42}); + let validator = crate::validator_for(&schema).expect("Invalid schema"); + let error = validator.validate(&instance).expect_err("Should fail"); + + // schema_path should have the unescaped key (type/name), re-escaped properly + assert_eq!(error.schema_path().as_str(), "/$defs/type~1name/type"); + } } diff --git a/crates/jsonschema/src/keywords/required.rs b/crates/jsonschema/src/keywords/required.rs index 39787958..7cf25e24 100644 --- a/crates/jsonschema/src/keywords/required.rs +++ b/crates/jsonschema/src/keywords/required.rs @@ -2,9 +2,9 @@ use crate::{ compiler, error::{no_error, ErrorIterator, ValidationError}, keywords::CompilationResult, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, types::JsonType, - validator::{Validate, ValidationContext}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use serde_json::{Map, Value}; @@ -22,8 +22,9 @@ impl RequiredValidator { Value::String(string) => required.push(string.clone()), _ => { return Err(ValidationError::single_type_error( - Location::new(), + location.clone(), location, + Location::new(), item, JsonType::String, )) @@ -52,6 +53,7 @@ impl Validate for RequiredValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, _ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(item) = instance { @@ -59,9 +61,9 @@ impl Validate for RequiredValidator { if !item.contains_key(property_name) { return Err(ValidationError::required( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, - // Value enum is needed for proper string escaping Value::String(property_name.clone()), )); } @@ -73,17 +75,19 @@ impl Validate for RequiredValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, _ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if let Value::Object(item) = instance { let mut errors = vec![]; + let evaluation_path = capture_evaluation_path(&self.location, evaluation_path); for property_name in &self.required { if !item.contains_key(property_name) { errors.push(ValidationError::required( self.location.clone(), + evaluation_path.clone(), location.into(), instance, - // Value enum is needed for proper string escaping Value::String(property_name.clone()), )); } @@ -116,14 +120,15 @@ impl Validate for SingleItemRequiredValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if !self.is_valid(instance, ctx) { return Err(ValidationError::required( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, - // Value enum is needed for proper string escaping Value::String(self.value.clone()), )); } @@ -166,8 +171,9 @@ pub(crate) fn compile_with_path( Some(SingleItemRequiredValidator::compile(item, location)) } else { Some(Err(ValidationError::single_type_error( - Location::new(), + location.clone(), location, + Location::new(), item, JsonType::String, ))) @@ -177,8 +183,9 @@ pub(crate) fn compile_with_path( } } _ => Some(Err(ValidationError::single_type_error( - Location::new(), + location.clone(), location, + Location::new(), schema, JsonType::Array, ))), diff --git a/crates/jsonschema/src/keywords/type_.rs b/crates/jsonschema/src/keywords/type_.rs index 0e346b7c..db0545b6 100644 --- a/crates/jsonschema/src/keywords/type_.rs +++ b/crates/jsonschema/src/keywords/type_.rs @@ -1,15 +1,16 @@ use crate::{ compiler, error::ValidationError, + evaluation::ErrorDescription, keywords::CompilationResult, paths::Location, types::{JsonType, JsonTypeSet}, - validator::{Validate, ValidationContext}, + validator::{capture_evaluation_path, EvaluationResult, Validate, ValidationContext}, }; use serde_json::{json, Map, Number, Value}; use std::str::FromStr; -use crate::paths::LazyLocation; +use crate::paths::{LazyLocation, LazyRefPath}; pub(crate) struct MultipleTypesValidator { types: JsonTypeSet, @@ -27,8 +28,9 @@ impl MultipleTypesValidator { types = types.insert(ty); } else { return Err(ValidationError::enumeration( - Location::new(), + location.clone(), location, + Location::new(), item, &json!([ "array", "boolean", "integer", "null", "number", "object", "string" @@ -38,6 +40,7 @@ impl MultipleTypesValidator { } _ => { return Err(ValidationError::single_type_error( + location.clone(), location, Location::new(), item, @@ -58,6 +61,7 @@ impl Validate for MultipleTypesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -65,12 +69,27 @@ impl Validate for MultipleTypesValidator { } else { Err(ValidationError::multiple_type_error( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, self.types, )) } } + fn evaluate( + &self, + instance: &Value, + _location: &LazyLocation, + _evaluation_path: &LazyRefPath, + ctx: &mut ValidationContext, + ) -> EvaluationResult { + if self.is_valid(instance, ctx) { + EvaluationResult::valid_empty() + } else { + let message = format!("{instance} is not of types {:?}", self.types); + EvaluationResult::invalid_empty(vec![ErrorDescription::new("type", message)]) + } + } } pub(crate) struct NullTypeValidator { @@ -92,6 +111,7 @@ impl Validate for NullTypeValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -99,12 +119,29 @@ impl Validate for NullTypeValidator { } else { Err(ValidationError::single_type_error( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, JsonType::Null, )) } } + fn evaluate( + &self, + instance: &Value, + _location: &LazyLocation, + _evaluation_path: &LazyRefPath, + ctx: &mut ValidationContext, + ) -> EvaluationResult { + if self.is_valid(instance, ctx) { + EvaluationResult::valid_empty() + } else { + EvaluationResult::invalid_empty(vec![ErrorDescription::new( + "type", + format!(r#"{instance} is not of type "null""#), + )]) + } + } } pub(crate) struct BooleanTypeValidator { @@ -126,6 +163,7 @@ impl Validate for BooleanTypeValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -133,12 +171,29 @@ impl Validate for BooleanTypeValidator { } else { Err(ValidationError::single_type_error( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, JsonType::Boolean, )) } } + fn evaluate( + &self, + instance: &Value, + _location: &LazyLocation, + _evaluation_path: &LazyRefPath, + ctx: &mut ValidationContext, + ) -> EvaluationResult { + if self.is_valid(instance, ctx) { + EvaluationResult::valid_empty() + } else { + EvaluationResult::invalid_empty(vec![ErrorDescription::new( + "type", + format!(r#"{instance} is not of type "boolean""#), + )]) + } + } } pub(crate) struct StringTypeValidator { @@ -161,6 +216,7 @@ impl Validate for StringTypeValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -168,12 +224,29 @@ impl Validate for StringTypeValidator { } else { Err(ValidationError::single_type_error( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, JsonType::String, )) } } + fn evaluate( + &self, + instance: &Value, + _location: &LazyLocation, + _evaluation_path: &LazyRefPath, + ctx: &mut ValidationContext, + ) -> EvaluationResult { + if self.is_valid(instance, ctx) { + EvaluationResult::valid_empty() + } else { + EvaluationResult::invalid_empty(vec![ErrorDescription::new( + "type", + format!(r#"{instance} is not of type "string""#), + )]) + } + } } pub(crate) struct ArrayTypeValidator { @@ -196,6 +269,7 @@ impl Validate for ArrayTypeValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -203,12 +277,29 @@ impl Validate for ArrayTypeValidator { } else { Err(ValidationError::single_type_error( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, JsonType::Array, )) } } + fn evaluate( + &self, + instance: &Value, + _location: &LazyLocation, + _evaluation_path: &LazyRefPath, + ctx: &mut ValidationContext, + ) -> EvaluationResult { + if self.is_valid(instance, ctx) { + EvaluationResult::valid_empty() + } else { + EvaluationResult::invalid_empty(vec![ErrorDescription::new( + "type", + format!(r#"{instance} is not of type "array""#), + )]) + } + } } pub(crate) struct ObjectTypeValidator { @@ -230,6 +321,7 @@ impl Validate for ObjectTypeValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -237,12 +329,29 @@ impl Validate for ObjectTypeValidator { } else { Err(ValidationError::single_type_error( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, JsonType::Object, )) } } + fn evaluate( + &self, + instance: &Value, + _location: &LazyLocation, + _evaluation_path: &LazyRefPath, + ctx: &mut ValidationContext, + ) -> EvaluationResult { + if self.is_valid(instance, ctx) { + EvaluationResult::valid_empty() + } else { + EvaluationResult::invalid_empty(vec![ErrorDescription::new( + "type", + format!(r#"{instance} is not of type "object""#), + )]) + } + } } pub(crate) struct NumberTypeValidator { @@ -264,6 +373,7 @@ impl Validate for NumberTypeValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -271,12 +381,29 @@ impl Validate for NumberTypeValidator { } else { Err(ValidationError::single_type_error( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, JsonType::Number, )) } } + fn evaluate( + &self, + instance: &Value, + _location: &LazyLocation, + _evaluation_path: &LazyRefPath, + ctx: &mut ValidationContext, + ) -> EvaluationResult { + if self.is_valid(instance, ctx) { + EvaluationResult::valid_empty() + } else { + EvaluationResult::invalid_empty(vec![ErrorDescription::new( + "type", + format!(r#"{instance} is not of type "number""#), + )]) + } + } } pub(crate) struct IntegerTypeValidator { @@ -302,6 +429,7 @@ impl Validate for IntegerTypeValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -309,12 +437,29 @@ impl Validate for IntegerTypeValidator { } else { Err(ValidationError::single_type_error( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, JsonType::Integer, )) } } + fn evaluate( + &self, + instance: &Value, + _location: &LazyLocation, + _evaluation_path: &LazyRefPath, + ctx: &mut ValidationContext, + ) -> EvaluationResult { + if self.is_valid(instance, ctx) { + EvaluationResult::valid_empty() + } else { + EvaluationResult::invalid_empty(vec![ErrorDescription::new( + "type", + format!(r#"{instance} is not of type "integer""#), + )]) + } + } } fn is_integer(num: &Number) -> bool { @@ -369,8 +514,9 @@ pub(crate) fn compile<'a>( Some(compile_single_type(ty.as_str(), location, item)) } else { Some(Err(ValidationError::single_type_error( - Location::new(), + location.clone(), location, + Location::new(), item, JsonType::String, ))) @@ -379,14 +525,18 @@ pub(crate) fn compile<'a>( Some(MultipleTypesValidator::compile(items, location)) } } - _ => Some(Err(ValidationError::multiple_type_error( - Location::new(), - ctx.location().clone(), - schema, - JsonTypeSet::empty() - .insert(JsonType::String) - .insert(JsonType::Array), - ))), + _ => { + let location = ctx.location().join("type"); + Some(Err(ValidationError::multiple_type_error( + location.clone(), + location, + Location::new(), + schema, + JsonTypeSet::empty() + .insert(JsonType::String) + .insert(JsonType::Array), + ))) + } } } @@ -404,8 +554,9 @@ fn compile_single_type<'a>( Ok(JsonType::Object) => ObjectTypeValidator::compile(location), Ok(JsonType::String) => StringTypeValidator::compile(location), Err(()) => Err(ValidationError::custom( - Location::new(), + location.clone(), location, + Location::new(), instance, "Unexpected type", )), diff --git a/crates/jsonschema/src/keywords/unevaluated_items.rs b/crates/jsonschema/src/keywords/unevaluated_items.rs index 0d40b069..4b7ed95f 100644 --- a/crates/jsonschema/src/keywords/unevaluated_items.rs +++ b/crates/jsonschema/src/keywords/unevaluated_items.rs @@ -14,9 +14,9 @@ use crate::{ compiler, evaluation::ErrorDescription, node::SchemaNode, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, thread::Shared, - validator::{EvaluationResult, Validate, ValidationContext}, + validator::{capture_evaluation_path, EvaluationResult, Validate, ValidationContext}, ValidationError, }; @@ -552,6 +552,7 @@ impl Validate for UnevaluatedItemsValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Array(items) = instance { @@ -577,6 +578,7 @@ impl Validate for UnevaluatedItemsValidator { if !unevaluated.is_empty() { return Err(ValidationError::unevaluated_items( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, unevaluated, @@ -590,6 +592,7 @@ impl Validate for UnevaluatedItemsValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Array(items) = instance { @@ -605,7 +608,12 @@ impl Validate for UnevaluatedItemsValidator { continue; } if let Some(validator) = &self.validators.unevaluated { - let child = validator.evaluate_instance(item, &location.push(idx), ctx); + let child = validator.evaluate_instance( + item, + &location.push(idx), + evaluation_path, + ctx, + ); if !child.valid { invalid = true; unevaluated.push(item.to_string()); @@ -622,6 +630,7 @@ impl Validate for UnevaluatedItemsValidator { errors.push(ErrorDescription::from_validation_error( &ValidationError::unevaluated_items( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, unevaluated, diff --git a/crates/jsonschema/src/keywords/unevaluated_properties.rs b/crates/jsonschema/src/keywords/unevaluated_properties.rs index 8994e6c1..9fcf0e01 100644 --- a/crates/jsonschema/src/keywords/unevaluated_properties.rs +++ b/crates/jsonschema/src/keywords/unevaluated_properties.rs @@ -15,9 +15,9 @@ use crate::{ compiler, ecma, evaluation::ErrorDescription, node::SchemaNode, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, thread::Shared, - validator::{EvaluationResult, Validate, ValidationContext}, + validator::{capture_evaluation_path, EvaluationResult, Validate, ValidationContext}, ValidationError, }; @@ -306,9 +306,11 @@ fn compile_pattern_properties<'a>( let schema_ctx = pat_ctx.new_at_location(pattern.as_str()); let Ok(regex) = ecma::to_rust_regex(pattern).and_then(|p| Regex::new(&p).map_err(|_| ())) else { + let location = schema_ctx.location().clone(); return Err(ValidationError::format( + location.clone(), + location, Location::new(), - ctx.location().clone(), schema, "regex", )); @@ -584,6 +586,7 @@ impl Validate for UnevaluatedPropertiesValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if let Value::Object(properties) = instance { @@ -618,6 +621,7 @@ impl Validate for UnevaluatedPropertiesValidator { if !unevaluated.is_empty() { return Err(ValidationError::unevaluated_properties( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, unevaluated, @@ -658,6 +662,7 @@ impl Validate for UnevaluatedPropertiesValidator { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if let Value::Object(properties) = instance { @@ -673,7 +678,12 @@ impl Validate for UnevaluatedPropertiesValidator { continue; } if let Some(validator) = &self.validators.unevaluated { - let child = validator.evaluate_instance(value, &location.push(property), ctx); + let child = validator.evaluate_instance( + value, + &location.push(property), + evaluation_path, + ctx, + ); if !child.valid { invalid = true; unevaluated.push(property.clone()); @@ -690,6 +700,7 @@ impl Validate for UnevaluatedPropertiesValidator { errors.push(ErrorDescription::from_validation_error( &ValidationError::unevaluated_properties( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, unevaluated, diff --git a/crates/jsonschema/src/keywords/unique_items.rs b/crates/jsonschema/src/keywords/unique_items.rs index 4d2c263e..12422411 100644 --- a/crates/jsonschema/src/keywords/unique_items.rs +++ b/crates/jsonschema/src/keywords/unique_items.rs @@ -4,12 +4,12 @@ use crate::{ ext::cmp, keywords::CompilationResult, paths::Location, - validator::{Validate, ValidationContext}, + validator::{capture_evaluation_path, Validate, ValidationContext}, }; use ahash::{AHashSet, AHasher}; use serde_json::{Map, Value}; -use crate::paths::LazyLocation; +use crate::paths::{LazyLocation, LazyRefPath}; use std::hash::{Hash, Hasher}; // Based on implementation proposed by Sven Marnach: @@ -122,6 +122,7 @@ impl Validate for UniqueItemsValidator { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance, ctx) { @@ -129,6 +130,7 @@ impl Validate for UniqueItemsValidator { } else { Err(ValidationError::unique_items( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, )) diff --git a/crates/jsonschema/src/lib.rs b/crates/jsonschema/src/lib.rs index ca0035ab..2194abf2 100644 --- a/crates/jsonschema/src/lib.rs +++ b/crates/jsonschema/src/lib.rs @@ -647,39 +647,26 @@ //! ```rust //! use jsonschema::{ //! paths::{LazyLocation, Location}, -//! Keyword, ValidationError, +//! Keyword, ValidationContext, ValidationError, //! }; //! use serde_json::{json, Map, Value}; -//! use std::iter::once; //! -//! // Step 1: Implement the Keyword trait //! struct EvenNumberValidator; //! //! impl Keyword for EvenNumberValidator { //! fn validate<'i>( //! &self, //! instance: &'i Value, -//! location: &LazyLocation, +//! instance_path: &LazyLocation, +//! ctx: &mut ValidationContext, +//! schema_path: &Location, //! ) -> Result<(), ValidationError<'i>> { -//! if let Value::Number(n) = instance { -//! if n.as_u64().map_or(false, |n| n % 2 == 0) { -//! Ok(()) -//! } else { -//! return Err(ValidationError::custom( -//! Location::new(), -//! location.into(), -//! instance, -//! "Number must be even", -//! )); +//! if let Some(n) = instance.as_u64() { +//! if n % 2 == 0 { +//! return Ok(()); //! } -//! } else { -//! Err(ValidationError::custom( -//! Location::new(), -//! location.into(), -//! instance, -//! "Value must be a number", -//! )) //! } +//! Err(ctx.custom_error(schema_path, instance_path, instance, "value must be an even integer")) //! } //! //! fn is_valid(&self, instance: &Value) -> bool { @@ -687,38 +674,27 @@ //! } //! } //! -//! // Step 2: Create a factory function -//! fn even_number_validator_factory<'a>( +//! fn even_number_factory<'a>( //! _parent: &'a Map, //! value: &'a Value, -//! path: Location, +//! schema_path: Location, //! ) -> Result, ValidationError<'a>> { -//! // You can use the `value` parameter to configure your validator if needed //! if value.as_bool() == Some(true) { //! Ok(Box::new(EvenNumberValidator)) //! } else { -//! Err(ValidationError::custom( -//! Location::new(), -//! path, -//! value, -//! "The 'even-number' keyword must be set to true", -//! )) +//! Err(ValidationError::schema(schema_path, value, "The 'even-number' keyword must be set to true")) //! } //! } //! -//! // Step 3: Use the custom keyword -//! fn main() -> Result<(), Box> { -//! let schema = json!({"even-number": true, "type": "integer"}); -//! let validator = jsonschema::options() -//! .with_keyword("even-number", even_number_validator_factory) -//! .build(&schema)?; -//! -//! assert!(validator.is_valid(&json!(2))); -//! assert!(!validator.is_valid(&json!(3))); -//! assert!(!validator.is_valid(&json!("not a number"))); +//! let schema = json!({"even-number": true, "type": "integer"}); +//! let validator = jsonschema::options() +//! .with_keyword("even-number", even_number_factory) +//! .build(&schema) +//! .expect("Invalid schema"); //! -//! Ok(()) -//! } +//! assert!(validator.is_valid(&json!(2))); +//! assert!(!validator.is_valid(&json!(3))); +//! assert!(!validator.is_valid(&json!("not a number"))); //! ``` //! //! In this example, we've created a custom `even-number` keyword that validates whether a number is even. @@ -729,11 +705,10 @@ //! //! ```rust //! # use jsonschema::{ -//! # paths::LazyLocation, -//! # Keyword, ValidationError, +//! # paths::{LazyLocation, Location}, +//! # Keyword, ValidationContext, ValidationError, //! # }; //! # use serde_json::{json, Map, Value}; -//! # use std::iter::once; //! # //! # struct EvenNumberValidator; //! # @@ -741,7 +716,9 @@ //! # fn validate<'i>( //! # &self, //! # instance: &'i Value, -//! # location: &LazyLocation, +//! # instance_path: &LazyLocation, +//! # ctx: &mut ValidationContext, +//! # schema_path: &Location, //! # ) -> Result<(), ValidationError<'i>> { //! # Ok(()) //! # } @@ -750,15 +727,13 @@ //! # true //! # } //! # } -//! # fn main() -> Result<(), Box> { //! let schema = json!({"even-number": true, "type": "integer"}); //! let validator = jsonschema::options() //! .with_keyword("even-number", |_, _, _| { //! Ok(Box::new(EvenNumberValidator)) //! }) -//! .build(&schema)?; -//! # Ok(()) -//! # } +//! .build(&schema) +//! .expect("Invalid schema"); //! ``` //! //! # Custom Formats @@ -919,7 +894,7 @@ pub use referencing::{ Draft, Error as ReferencingError, Registry, RegistryOptions, Resource, Retrieve, Uri, }; pub use types::{JsonType, JsonTypeSet, JsonTypeSetIterator}; -pub use validator::Validator; +pub use validator::{ValidationContext, Validator}; #[cfg(feature = "resolve-async")] pub use referencing::AsyncRetrieve; @@ -2622,6 +2597,12 @@ pub(crate) mod tests_util { assert_eq!(error.schema_path().as_str(), expected); } + #[track_caller] + pub(crate) fn assert_evaluation_path(schema: &Value, instance: &Value, expected: &str) { + let error = validate(schema, instance); + assert_eq!(error.evaluation_path().as_str(), expected); + } + #[track_caller] pub(crate) fn assert_locations(schema: &Value, instance: &Value, expected: &[&str]) { let validator = crate::validator_for(schema).unwrap(); diff --git a/crates/jsonschema/src/node.rs b/crates/jsonschema/src/node.rs index 74208787..c127645d 100644 --- a/crates/jsonschema/src/node.rs +++ b/crates/jsonschema/src/node.rs @@ -1,17 +1,19 @@ use crate::{ compiler::Context, error::ErrorIterator, - evaluation::{format_schema_location, Annotations, EvaluationNode}, + evaluation::{Annotations, EvaluationNode}, keywords::{BoxedValidator, Keyword}, - paths::{LazyLocation, Location}, + paths::{LazyLocation, LazyRefPath, Location}, thread::{Shared, SharedWeak}, - validator::{EvaluationResult, Validate, ValidationContext}, + validator::{ + capture_evaluation_path, compute_final_evaluation_path, CapturedRefState, EvaluationResult, + Validate, ValidationContext, + }, ValidationError, }; use referencing::Uri; use serde_json::Value; use std::{ - cell::OnceCell, fmt, sync::{Arc, OnceLock}, }; @@ -161,12 +163,13 @@ impl Validate for PendingSchemaNode { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { if ctx.enter(self.node_id(), instance) { return Ok(()); } - let result = self.with_node(|node| node.validate(instance, location, ctx)); + let result = self.with_node(|node| node.validate(instance, location, evaluation_path, ctx)); ctx.exit(self.node_id(), instance); result } @@ -175,12 +178,14 @@ impl Validate for PendingSchemaNode { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { if ctx.enter(self.node_id(), instance) { return crate::error::no_error(); } - let result = self.with_node(|node| node.iter_errors(instance, location, ctx)); + let result = + self.with_node(|node| node.iter_errors(instance, location, evaluation_path, ctx)); ctx.exit(self.node_id(), instance); result } @@ -189,12 +194,13 @@ impl Validate for PendingSchemaNode { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { if ctx.enter(self.node_id(), instance) { return EvaluationResult::valid_empty(); } - let result = self.with_node(|node| node.evaluate(instance, location, ctx)); + let result = self.with_node(|node| node.evaluate(instance, location, evaluation_path, ctx)); ctx.exit(self.node_id(), instance); result } @@ -259,18 +265,6 @@ impl SchemaNode { } } - pub(crate) fn clone_with_location( - &self, - location: Location, - absolute_path: Option>>, - ) -> SchemaNode { - SchemaNode { - validators: self.validators.clone(), - location, - absolute_path, - } - } - pub(crate) fn validators(&self) -> impl ExactSizeIterator { match self.validators.as_ref() { NodeValidators::Boolean { validator } => { @@ -293,16 +287,29 @@ impl SchemaNode { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationNode { let instance_location: Location = location.into(); - let schema_location = format_schema_location(&self.location, self.absolute_path.as_ref()); - match self.evaluate(instance, location, ctx) { + + // When no $ref on stack, evaluation_path == schema_path + let keyword_location = if evaluation_path.is_empty() { + self.location.clone() + } else { + let captured = CapturedRefState::from(evaluation_path); + compute_final_evaluation_path(&self.location, &captured) + }; + + // schemaLocation: The canonical URI of the schema keyword, WITHOUT $ref traversals. + let schema_location = + ctx.format_schema_location(&self.location, self.absolute_path.as_ref()); + + match self.evaluate(instance, location, evaluation_path, ctx) { EvaluationResult::Valid { annotations, children, } => EvaluationNode::valid( - self.location.clone(), + keyword_location, self.absolute_path.clone(), schema_location, instance_location, @@ -314,7 +321,7 @@ impl SchemaNode { children, annotations, } => EvaluationNode::invalid( - self.location.clone(), + keyword_location, self.absolute_path.clone(), schema_location, instance_location, @@ -329,6 +336,7 @@ impl SchemaNode { fn evaluate_subschemas<'a, I>( instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, subschemas: I, annotations: Option, ctx: &mut ValidationContext, @@ -342,29 +350,48 @@ impl SchemaNode { ), > + 'a, { + // Fast path check: capture ref state only if needed + let captured = if evaluation_path.is_empty() { + None + } else { + Some(CapturedRefState::from(evaluation_path)) + }; + let (lower_bound, _) = subschemas.size_hint(); let mut children: Vec = Vec::with_capacity(lower_bound); let mut invalid = false; - let instance_location: OnceCell = OnceCell::new(); + + // Compute instance location ONCE before loop - each child clones it + let instance_loc: Location = location.into(); for (child_location, absolute_location, validator) in subschemas { - let child_result = validator.evaluate(instance, location, ctx); + let child_result = validator.evaluate(instance, location, evaluation_path, ctx); - let schema_location = child_location.clone(); let absolute_location = absolute_location.cloned(); - let instance_loc = instance_location.get_or_init(|| location.into()).clone(); + + // evaluationPath: fast path when no $ref on stack (just Arc clone) + let evaluation_path = match &captured { + None => child_location.clone(), + Some(cap) => compute_final_evaluation_path(child_location, cap), + }; + + // schemaLocation: The canonical location WITHOUT $ref traversals. + // Per JSON Schema spec: "MUST NOT include by-reference applicators such as $ref" + // For by-reference validators like $ref, use the target's canonical location. + // For regular validators, use the keyword's location. + let schema_location = validator.canonical_location().unwrap_or(child_location); let formatted_schema_location = - format_schema_location(&schema_location, absolute_location.as_ref()); + ctx.format_schema_location(schema_location, absolute_location.as_ref()); let child_node = match child_result { EvaluationResult::Valid { annotations, children, } => EvaluationNode::valid( - schema_location, + evaluation_path, absolute_location, formatted_schema_location, - instance_loc, + instance_loc.clone(), annotations, children, ), @@ -375,10 +402,10 @@ impl SchemaNode { } => { invalid = true; EvaluationNode::invalid( - schema_location, + evaluation_path, absolute_location, formatted_schema_location, - instance_loc, + instance_loc.clone(), annotations, errors, children, @@ -436,22 +463,28 @@ impl Validate for SchemaNode { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>> { match self.validators.as_ref() { NodeValidators::Keyword(kvs) => { for entry in &kvs.validators { - entry.validator.validate(instance, location, ctx)?; + entry + .validator + .validate(instance, location, evaluation_path, ctx)?; } } NodeValidators::Array { validators } => { for entry in validators { - entry.validator.validate(instance, location, ctx)?; + entry + .validator + .validate(instance, location, evaluation_path, ctx)?; } } NodeValidators::Boolean { validator: Some(_) } => { return Err(ValidationError::false_schema( self.location.clone(), + capture_evaluation_path(&self.location, evaluation_path), location.into(), instance, )); @@ -465,29 +498,38 @@ impl Validate for SchemaNode { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { match self.validators.as_ref() { NodeValidators::Keyword(kvs) if kvs.validators.len() == 1 => kvs.validators[0] .validator - .iter_errors(instance, location, ctx), + .iter_errors(instance, location, evaluation_path, ctx), NodeValidators::Keyword(kvs) => ErrorIterator::from_iterator( kvs.validators .iter() - .flat_map(|entry| entry.validator.iter_errors(instance, location, ctx)) + .flat_map(|entry| { + entry + .validator + .iter_errors(instance, location, evaluation_path, ctx) + }) .collect::>() .into_iter(), ), NodeValidators::Boolean { validator: Some(v), .. - } => v.iter_errors(instance, location, ctx), + } => v.iter_errors(instance, location, evaluation_path, ctx), NodeValidators::Boolean { validator: None, .. } => ErrorIterator::from_iterator(std::iter::empty()), NodeValidators::Array { validators } => ErrorIterator::from_iterator( validators .iter() - .flat_map(move |entry| entry.validator.iter_errors(instance, location, ctx)) + .flat_map(move |entry| { + entry + .validator + .iter_errors(instance, location, evaluation_path, ctx) + }) .collect::>() .into_iter(), ), @@ -498,12 +540,14 @@ impl Validate for SchemaNode { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { match self.validators.as_ref() { NodeValidators::Array { ref validators } => Self::evaluate_subschemas( instance, location, + evaluation_path, validators.iter().map(|entry| { ( &entry.location, @@ -516,7 +560,7 @@ impl Validate for SchemaNode { ), NodeValidators::Boolean { ref validator } => { if let Some(validator) = validator { - validator.evaluate(instance, location, ctx) + validator.evaluate(instance, location, evaluation_path, ctx) } else { EvaluationResult::Valid { annotations: None, @@ -535,6 +579,7 @@ impl Validate for SchemaNode { Self::evaluate_subschemas( instance, location, + evaluation_path, validators.iter().map(|entry| { ( &entry.location, diff --git a/crates/jsonschema/src/options.rs b/crates/jsonschema/src/options.rs index 9b854aaa..e3e2e5c4 100644 --- a/crates/jsonschema/src/options.rs +++ b/crates/jsonschema/src/options.rs @@ -447,10 +447,9 @@ impl ValidationOptions { /// ```rust /// # use jsonschema::{ /// # paths::{LazyLocation, Location}, - /// # ErrorIterator, Keyword, ValidationError, + /// # Keyword, ValidationContext, ValidationError, /// # }; /// # use serde_json::{json, Map, Value}; - /// # use std::iter::once; /// /// struct MyCustomValidator; /// @@ -458,31 +457,25 @@ impl ValidationOptions { /// fn validate<'i>( /// &self, /// instance: &'i Value, - /// location: &LazyLocation, + /// instance_path: &LazyLocation, + /// ctx: &mut ValidationContext, + /// schema_path: &Location, /// ) -> Result<(), ValidationError<'i>> { - /// // ... validate instance ... /// if !instance.is_object() { - /// return Err(ValidationError::custom( - /// Location::new(), - /// location.into(), - /// instance, - /// "Boom!", - /// )); - /// } else { - /// Ok(()) + /// return Err(ctx.custom_error(schema_path, instance_path, instance, "expected an object")); /// } + /// Ok(()) /// } + /// /// fn is_valid(&self, instance: &Value) -> bool { - /// // ... determine if instance is valid ... - /// true + /// instance.is_object() /// } /// } /// - /// // You can create a factory function, or use a closure to create new validator instances. /// fn custom_validator_factory<'a>( - /// parent: &'a Map, - /// value: &'a Value, - /// path: Location, + /// _parent: &'a Map, + /// _value: &'a Value, + /// _schema_path: Location, /// ) -> Result, ValidationError<'a>> { /// Ok(Box::new(MyCustomValidator)) /// } diff --git a/crates/jsonschema/src/paths.rs b/crates/jsonschema/src/paths.rs index 32f376e2..fe6b643c 100644 --- a/crates/jsonschema/src/paths.rs +++ b/crates/jsonschema/src/paths.rs @@ -63,6 +63,10 @@ impl<'a> LazyLocation<'a, '_> { impl<'a> From<&'a LazyLocation<'_, '_>> for Location { fn from(value: &'a LazyLocation<'_, '_>) -> Self { + // Stack capacity for typical paths (≤16 segments), fallback to Vec for deep paths + const STACK_CAPACITY: usize = 16; + + // First pass: count segments and calculate string capacity let mut capacity = 0; let mut string_capacity = 0; let mut head = value; @@ -78,29 +82,65 @@ impl<'a> From<&'a LazyLocation<'_, '_>> for Location { let mut buffer = String::with_capacity(string_capacity); - let mut segments = Vec::with_capacity(capacity); - head = value; + if capacity <= STACK_CAPACITY { + // Stack-allocated storage with references - no cloning needed + let mut stack_segments: [Option<&LocationSegment<'_>>; STACK_CAPACITY] = + [None; STACK_CAPACITY]; + let mut idx = 0; + head = value; - if head.parent.is_some() { - segments.push(head.segment.clone()); - } + if head.parent.is_some() { + stack_segments[idx] = Some(&head.segment); + idx += 1; + } + + while let Some(next) = head.parent { + head = next; + if head.parent.is_some() { + stack_segments[idx] = Some(&head.segment); + idx += 1; + } + } + + // Format in reverse order + for segment in stack_segments[..idx].iter().rev().flatten() { + buffer.push('/'); + match segment { + LocationSegment::Property(property) => { + write_escaped_str(&mut buffer, property); + } + LocationSegment::Index(idx) => { + let mut itoa_buffer = itoa::Buffer::new(); + buffer.push_str(itoa_buffer.format(*idx)); + } + } + } + } else { + // Heap-allocated fallback for deep paths (>16 segments) + let mut segments: Vec<&LocationSegment<'_>> = Vec::with_capacity(capacity); + head = value; - while let Some(next) = head.parent { - head = next; if head.parent.is_some() { - segments.push(head.segment.clone()); + segments.push(&head.segment); } - } - for segment in segments.iter().rev() { - buffer.push('/'); - match segment { - LocationSegment::Property(property) => { - write_escaped_str(&mut buffer, property); + while let Some(next) = head.parent { + head = next; + if head.parent.is_some() { + segments.push(&head.segment); } - LocationSegment::Index(idx) => { - let mut itoa_buffer = itoa::Buffer::new(); - buffer.push_str(itoa_buffer.format(*idx)); + } + + for segment in segments.iter().rev() { + buffer.push('/'); + match segment { + LocationSegment::Property(property) => { + write_escaped_str(&mut buffer, property); + } + LocationSegment::Index(idx) => { + let mut itoa_buffer = itoa::Buffer::new(); + buffer.push_str(itoa_buffer.format(*idx)); + } } } } @@ -109,6 +149,111 @@ impl<'a> From<&'a LazyLocation<'_, '_>> for Location { } } +/// A lazily constructed ref path for evaluation path tracking. +/// +/// Like [`LazyLocation`], uses stack-based references - no heap allocation. +/// Each `$ref` traversal creates a new stack-local entry that borrows from +/// the parent, automatically "popping" when the function returns. +pub(crate) struct LazyRefPath<'a, 'b> { + /// Location of the $ref keyword. `None` for root. + pub(crate) ref_location: Option<&'a Location>, + /// Base path to strip when computing evaluation path. `None` for root. + pub(crate) strip_base: Option<&'a Location>, + pub(crate) parent: Option<&'b LazyRefPath<'b, 'a>>, + /// Cached evaluation prefix - computed lazily on first access. + /// Uses Cell for interior mutability without synchronization overhead. + cached_prefix: std::cell::Cell>, +} + +impl std::fmt::Debug for LazyRefPath<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LazyRefPath") + .field("ref_location", &self.ref_location) + .field("strip_base", &self.strip_base) + .field("parent", &self.parent.map(|_| "...")) + .finish() + } +} + +impl<'a> LazyRefPath<'a, '_> { + /// Create root (empty) ref path. + #[must_use] + pub(crate) const fn new() -> Self { + LazyRefPath { + ref_location: None, + strip_base: None, + parent: None, + cached_prefix: std::cell::Cell::new(None), + } + } + + /// Push a new $ref onto the path. + #[inline] + #[must_use] + pub(crate) fn push(&'a self, ref_location: &'a Location, strip_base: &'a Location) -> Self { + LazyRefPath { + ref_location: Some(ref_location), + strip_base: Some(strip_base), + parent: Some(self), + cached_prefix: std::cell::Cell::new(None), + } + } + + /// Check if this is the root (no $refs). + #[inline] + #[must_use] + pub(crate) fn is_empty(&self) -> bool { + self.parent.is_none() + } + + /// Get the cached evaluation prefix, computing it if necessary. + /// This is O(1) after first call thanks to Cell caching. + /// Parent prefixes are also cached, so computation is incremental. + /// Returns a clone of the cached Location (cheap due to Arc). + #[inline] + pub(crate) fn get_eval_prefix(&self) -> Location { + // Take the cached value to check it + if let Some(prefix) = self.cached_prefix.take() { + // Put it back and return + self.cached_prefix.set(Some(prefix.clone())); + prefix + } else { + // Compute the prefix + let prefix = self.compute_eval_prefix_inner(); + self.cached_prefix.set(Some(prefix.clone())); + prefix + } + } + + /// Compute the evaluation prefix for this node. + /// Uses parent's cached prefix for O(1) incremental computation. + fn compute_eval_prefix_inner(&self) -> Location { + let Some(parent) = self.parent else { + // Root node - empty prefix (shouldn't normally be called) + return Location::new(); + }; + + let ref_location = self.ref_location.unwrap(); + + // Check if parent is root (no grandparent) + if parent.parent.is_none() { + // This is the first $ref - prefix is just the ref_location + return ref_location.clone(); + } + + // Parent has its own ref - get parent's prefix and extend it + let parent_prefix = parent.get_eval_prefix(); + let prev_strip_base = parent.strip_base.unwrap(); + + if let Some(suffix) = ref_location.as_str().strip_prefix(prev_strip_base.as_str()) { + // Append suffix directly - it's already a valid JSON pointer path + parent_prefix.join_raw_suffix(suffix) + } else { + parent_prefix.clone() + } + } +} + impl<'a> From<&'a Keyword> for LocationSegment<'a> { fn from(value: &'a Keyword) -> Self { match value { @@ -165,6 +310,26 @@ impl Location { pub fn new() -> Self { Self(Arc::from("")) } + + pub(crate) fn from_escaped(escaped: &str) -> Self { + Self(Arc::from(escaped)) + } + + /// Append a raw JSON pointer suffix (already escaped). + /// This is more efficient than multiple `join` calls when the suffix + /// is already a valid JSON pointer path. + #[must_use] + pub(crate) fn join_raw_suffix(&self, suffix: &str) -> Self { + if suffix.is_empty() { + return self.clone(); + } + let parent = &self.0; + let mut buffer = String::with_capacity(parent.len() + suffix.len()); + buffer.push_str(parent); + buffer.push_str(suffix); + Self(Arc::from(buffer)) + } + #[must_use] pub fn join<'a>(&self, segment: impl Into>) -> Self { let parent = &self.0; @@ -177,9 +342,13 @@ impl Location { Self(Arc::from(buffer)) } LocationSegment::Index(idx) => { - let mut buffer = itoa::Buffer::new(); - let segment = buffer.format(idx); - Self(Arc::from(format!("{parent}/{segment}"))) + let mut itoa_buf = itoa::Buffer::new(); + let segment = itoa_buf.format(idx); + let mut buffer = String::with_capacity(parent.len() + segment.len() + 1); + buffer.push_str(parent); + buffer.push('/'); + buffer.push_str(segment); + Self(Arc::from(buffer)) } } } diff --git a/crates/jsonschema/src/properties.rs b/crates/jsonschema/src/properties.rs index 73177139..fccfeda7 100644 --- a/crates/jsonschema/src/properties.rs +++ b/crates/jsonschema/src/properties.rs @@ -1,5 +1,6 @@ use crate::{ compiler, node::SchemaNode, paths::Location, thread::ThreadBound, validator::Validate as _, + ValidationContext, }; use ahash::AHashMap; use serde_json::{Map, Value}; @@ -93,12 +94,12 @@ pub(crate) fn compile_big_map<'a>( pub(crate) fn are_properties_valid( prop_map: &M, props: &Map, - ctx: &mut crate::validator::ValidationContext, + ctx: &mut ValidationContext, check: F, ) -> bool where M: PropertiesValidatorsMap, - F: Fn(&Value, &mut crate::validator::ValidationContext) -> bool, + F: Fn(&Value, &mut ValidationContext) -> bool, { for (property, instance) in props { if let Some(validator) = prop_map.get_validator(property) { @@ -123,7 +124,14 @@ pub(crate) fn compile_fancy_regex_patterns<'a>( for (pattern, subschema) in obj { let pctx = kctx.new_at_location(pattern.as_str()); let compiled_pattern = ctx.get_or_compile_regex(pattern).map_err(|()| { - ValidationError::format(Location::new(), kctx.location().clone(), subschema, "regex") + let location = kctx.location().clone(); + ValidationError::format( + location.clone(), + location, + Location::new(), + subschema, + "regex", + ) })?; let node = compiler::compile(&pctx, pctx.as_resource_ref(subschema))?; compiled_patterns.push(((*compiled_pattern).clone(), node)); @@ -141,7 +149,14 @@ pub(crate) fn compile_regex_patterns<'a>( for (pattern, subschema) in obj { let pctx = kctx.new_at_location(pattern.as_str()); let compiled_pattern = ctx.get_or_compile_standard_regex(pattern).map_err(|()| { - ValidationError::format(Location::new(), kctx.location().clone(), subschema, "regex") + let location = kctx.location().clone(); + ValidationError::format( + location.clone(), + location, + Location::new(), + subschema, + "regex", + ) })?; let node = compiler::compile(&pctx, pctx.as_resource_ref(subschema))?; compiled_patterns.push(((*compiled_pattern).clone(), node)); @@ -150,20 +165,22 @@ pub(crate) fn compile_regex_patterns<'a>( } macro_rules! compile_dynamic_prop_map_validator { - ($validator:tt, $properties:ident, $( $arg:expr ),* $(,)*) => {{ + ($validator:tt, $properties:ident, $ctx:expr, $( $arg:expr ),* $(,)*) => {{ if let Value::Object(map) = $properties { if map.len() < 40 { Some($validator::::compile( - map, $($arg, )* + map, $ctx, $($arg, )* )) } else { Some($validator::::compile( - map, $($arg, )* + map, $ctx, $($arg, )* )) } } else { + let location = $ctx.location().clone(); Some(Err(ValidationError::custom( - Location::new(), + location.clone(), + location, Location::new(), $properties, "Unexpected type", diff --git a/crates/jsonschema/src/tracing.rs b/crates/jsonschema/src/tracing.rs new file mode 100644 index 00000000..5bcd1706 --- /dev/null +++ b/crates/jsonschema/src/tracing.rs @@ -0,0 +1,73 @@ +use crate::paths::{LazyLocation, Location}; + +/// Context information passed to tracing callbacks during schema validation. +/// +/// This struct provides information about the validation state at a specific point +/// in the validation tree, including the instance location, schema location, and +/// the evaluation result. +#[derive(Debug, Clone)] +pub struct TracingContext<'a, 'b, 'c> { + /// The location in the instance being validated + pub instance_location: &'c LazyLocation<'a, 'b>, + /// The location in the schema performing the validation + pub schema_location: &'c Location, + /// The result of evaluating this node + pub result: NodeEvaluationResult, +} + +impl<'a, 'b, 'c> TracingContext<'a, 'b, 'c> { + /// Create a new tracing context + pub fn new( + instance_location: &'c LazyLocation<'a, 'b>, + schema_location: &'c Location, + result: impl Into, + ) -> Self { + Self { + instance_location, + schema_location, + result: result.into(), + } + } + + /// Call the tracing callback with this context + pub fn call(self, callback: TracingCallback<'_>) { + callback(self); + } +} + +/// Result of evaluating a schema node against an instance. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NodeEvaluationResult { + /// The validation passed + Valid, + /// The validation failed + Invalid, + /// The validation was not applicable (e.g., type mismatch) + Ignored, +} + +impl From for NodeEvaluationResult { + fn from(value: bool) -> Self { + if value { + Self::Valid + } else { + Self::Invalid + } + } +} + +impl From> for NodeEvaluationResult { + fn from(value: Option) -> Self { + match value { + Some(true) => Self::Valid, + Some(false) => Self::Invalid, + None => Self::Ignored, + } + } +} + +/// Type alias for tracing callbacks. +/// +/// A tracing callback is called for each node in the validation tree, +/// providing visibility into the validation process. +pub type TracingCallback<'a> = &'a mut dyn FnMut(TracingContext); diff --git a/crates/jsonschema/src/validator.rs b/crates/jsonschema/src/validator.rs index 08717e1a..7705d6e7 100644 --- a/crates/jsonschema/src/validator.rs +++ b/crates/jsonschema/src/validator.rs @@ -5,16 +5,166 @@ use crate::{ error::{error, no_error, ErrorIterator}, evaluation::{Annotations, ErrorDescription, Evaluation, EvaluationNode}, node::SchemaNode, - paths::LazyLocation, + paths::{LazyLocation, LazyRefPath, Location}, thread::ThreadBound, Draft, ValidationError, ValidationOptions, }; +use ahash::AHashMap; +use referencing::Uri; use serde_json::Value; +use std::sync::{Arc, OnceLock}; + +/// Captured state for lazy evaluation path computation. +/// This is stored in `LazyEvaluationPath::Deferred` and contains +/// the precomputed `eval_prefix` (computed at capture time, not push time). +#[derive(Clone)] +pub(crate) struct CapturedRefState { + /// Precomputed evaluation path prefix. + eval_prefix: Location, + /// Base location to strip from `schema_location`. + strip_base: Location, +} + +impl From<&LazyRefPath<'_, '_>> for CapturedRefState { + /// Gets cached `eval_prefix` from `LazyRefPath` (O(1) after first call). + fn from(path: &LazyRefPath<'_, '_>) -> Self { + debug_assert!( + !path.is_empty(), + "CapturedRefState::from called on empty LazyRefPath" + ); + + CapturedRefState { + eval_prefix: path.get_eval_prefix(), + strip_base: path.strip_base.unwrap().clone(), + } + } +} + +/// A lazily-evaluated evaluation path. +/// +/// This enum allows deferring the expensive string operations needed to compute +/// evaluation paths until the error is actually displayed. When validation fails +/// in composition validators like `anyOf`, many errors are collected but most are +/// never displayed - lazy evaluation avoids wasted work. +pub(crate) enum LazyEvaluationPath { + /// Fast path: no $ref on stack, `schema_location` IS the evaluation path. + /// Cost: just returning a reference. + Direct(Location), + + /// Deferred: need to transform through $ref stack. + /// We capture the precomputed `eval_prefix` at error creation time. + /// The `OnceLock` caches the final computed result for subsequent accesses. + Deferred { + schema_location: Location, + /// Captured state with precomputed `eval_prefix` (computed at capture time). + captured: CapturedRefState, + /// Cached computed result (thread-safe). + cached: OnceLock, + }, +} + +impl std::fmt::Debug for LazyEvaluationPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LazyEvaluationPath::Direct(loc) => f.debug_tuple("Direct").field(loc).finish(), + LazyEvaluationPath::Deferred { cached, .. } => { + // Only show the cached value if already computed + f.debug_struct("Deferred") + .field("resolved", &cached.get()) + .finish() + } + } + } +} + +impl Clone for LazyEvaluationPath { + fn clone(&self) -> Self { + match self { + LazyEvaluationPath::Direct(loc) => LazyEvaluationPath::Direct(loc.clone()), + LazyEvaluationPath::Deferred { + schema_location, + captured, + cached, + } => { + // Clone the cached value if present, otherwise create new empty lock + let new_cached = OnceLock::new(); + if let Some(val) = cached.get() { + let _ = new_cached.set(val.clone()); + } + LazyEvaluationPath::Deferred { + schema_location: schema_location.clone(), + captured: captured.clone(), + cached: new_cached, + } + } + } + } +} + +impl From for LazyEvaluationPath { + #[inline] + fn from(location: Location) -> Self { + LazyEvaluationPath::Direct(location) + } +} + +impl LazyEvaluationPath { + /// Create a new Deferred lazy evaluation path. + #[inline] + pub(crate) fn deferred(schema_location: Location, captured: CapturedRefState) -> Self { + LazyEvaluationPath::Deferred { + schema_location, + captured, + cached: OnceLock::new(), + } + } + + /// Resolve the lazy evaluation path to a reference to the Location. + /// + /// For `Direct` paths, this returns the inner reference directly. + /// For `Deferred` paths, this computes the result on first call and caches it. + #[inline] + #[must_use] + pub(crate) fn resolve(&self) -> &Location { + match self { + LazyEvaluationPath::Direct(loc) => loc, + LazyEvaluationPath::Deferred { + schema_location, + captured, + cached, + } => cached.get_or_init(|| compute_final_evaluation_path(schema_location, captured)), + } + } +} -/// Tracks `(node_id, instance_ptr)` pairs to detect cycles in circular `$ref` chains. +/// Compute final evaluation path from schema location and captured state. +#[inline] +pub(crate) fn compute_final_evaluation_path( + schema_location: &Location, + captured: &CapturedRefState, +) -> Location { + let schema_str = schema_location.as_str(); + let base_str = captured.strip_base.as_str(); + + let Some(suffix) = schema_str.strip_prefix(base_str) else { + return schema_location.clone(); + }; + + // Fast path: if suffix is empty, just return eval_prefix (no allocation) + if suffix.is_empty() { + return captured.eval_prefix.clone(); + } + + captured.eval_prefix.join_raw_suffix(suffix) +} + +/// Context for `validate()`, `iter_errors()`, and `evaluate()` operations. +/// +/// Tracks cycle detection during validation. #[derive(Default)] -pub(crate) struct ValidationContext { +pub struct ValidationContext { validating: Vec<(usize, usize)>, + schema_location_cache: AHashMap<(usize, usize), Arc>, } impl ValidationContext { @@ -39,9 +189,68 @@ impl ValidationContext { debug_assert_eq!( popped, Some((node_id, std::ptr::from_ref::(instance) as usize)), - "ValidationContext::exit called out of order" + "LightweightContext::exit called out of order" ); } + + /// Get or compute a formatted schema location. + #[inline] + pub(crate) fn format_schema_location( + &mut self, + location: &Location, + absolute: Option<&Arc>>, + ) -> Arc { + // Create cache key from string pointer (stable for Arc) + let loc_ptr = location.as_str().as_ptr() as usize; + let uri_ptr = absolute.map_or(0, |u| Arc::as_ptr(u) as usize); + let key = (loc_ptr, uri_ptr); + + if let Some(cached) = self.schema_location_cache.get(&key) { + return Arc::clone(cached); + } + + let result = crate::evaluation::format_schema_location(location, absolute); + self.schema_location_cache.insert(key, Arc::clone(&result)); + result + } + + /// Create a validation error for custom keywords. + /// + /// Note: Custom keywords don't participate in `$ref` resolution, so the evaluation + /// path is simply the schema path without any ref-related transformations. + #[must_use] + pub fn custom_error<'i>( + &self, + schema_path: &Location, + instance_path: &LazyLocation, + instance: &'i Value, + message: &'static str, + ) -> ValidationError<'i> { + ValidationError::custom( + schema_path.clone(), + // Custom keywords are leaf validators, so schema_path == evaluation_path + LazyEvaluationPath::from(schema_path.clone()), + instance_path.into(), + instance, + message, + ) + } +} + +/// Capture state for lazy evaluation path computation. +#[inline] +pub(crate) fn capture_evaluation_path( + schema_location: &Location, + evaluation_path: &LazyRefPath, +) -> LazyEvaluationPath { + if evaluation_path.is_empty() { + // Fast path: direct mapping, no transformation needed + schema_location.clone().into() + } else { + // Compute captured state by walking the ref path chain + let captured = CapturedRefState::from(evaluation_path); + LazyEvaluationPath::deferred(schema_location.clone(), captured) + } } /// The Validate trait represents a predicate over some JSON value. Some validators are very simple @@ -59,16 +268,19 @@ impl ValidationContext { /// `is_valid`. `evaluate` is only necessary for validators which compose other validators. See the /// documentation for `evaluate` for more information. /// -/// All methods accept a `ValidationContext` parameter for cycle detection in circular `$ref` -/// chains. Simple validators that don't recurse into child schemas can ignore this parameter. +/// # Context Types +/// +/// - `is_valid` takes `LightweightContext`: Only cycle detection, zero path tracking overhead. +/// - `validate`, `iter_errors`, `evaluate` take `ValidationContext`: Cycle detection + evaluation path tracking. pub(crate) trait Validate: ThreadBound { fn iter_errors<'i>( &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> ErrorIterator<'i> { - match self.validate(instance, location, ctx) { + match self.validate(instance, location, evaluation_path, ctx) { Ok(()) => no_error(), Err(err) => error(err), } @@ -80,6 +292,7 @@ pub(crate) trait Validate: ThreadBound { &self, instance: &'i Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> Result<(), ValidationError<'i>>; @@ -87,10 +300,11 @@ pub(crate) trait Validate: ThreadBound { &self, instance: &Value, location: &LazyLocation, + evaluation_path: &LazyRefPath, ctx: &mut ValidationContext, ) -> EvaluationResult { let errors: Vec = self - .iter_errors(instance, location, ctx) + .iter_errors(instance, location, evaluation_path, ctx) .map(|e| ErrorDescription::from_validation_error(&e)) .collect(); if errors.is_empty() { @@ -99,6 +313,19 @@ pub(crate) trait Validate: ThreadBound { EvaluationResult::invalid_empty(errors) } } + + /// Returns the canonical location for this validator's schemaLocation output. + /// + /// Per JSON Schema spec, schemaLocation "MUST NOT include by-reference applicators + /// such as `$ref` or `$dynamicRef`". For most validators, the keyword location is the + /// canonical location, so this returns `None` by default. + /// + /// `RefValidator` and similar by-reference validators override this to return + /// the target schema's canonical location (e.g., `/$defs/item` instead of + /// `/properties/foo/$ref`). + fn canonical_location(&self) -> Option<&Location> { + None + } } /// The result of evaluating a validator against an instance. This is a "partial" result because it does not include information about @@ -284,15 +511,18 @@ impl Validator { #[inline] pub fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> { let mut ctx = ValidationContext::new(); - self.root.validate(instance, &LazyLocation::new(), &mut ctx) + let evaluation_path = LazyRefPath::new(); + self.root + .validate(instance, &LazyLocation::new(), &evaluation_path, &mut ctx) } /// Run validation against `instance` and return an iterator over [`ValidationError`] in the error case. #[inline] #[must_use] pub fn iter_errors<'i>(&'i self, instance: &'i Value) -> ErrorIterator<'i> { let mut ctx = ValidationContext::new(); + let evaluation_path = LazyRefPath::new(); self.root - .iter_errors(instance, &LazyLocation::new(), &mut ctx) + .iter_errors(instance, &LazyLocation::new(), &evaluation_path, &mut ctx) } /// Run validation against `instance` but return a boolean result instead of an iterator. /// It is useful for cases, where it is important to only know the fact if the data is valid or not. @@ -308,9 +538,10 @@ impl Validator { #[inline] pub fn evaluate(&self, instance: &Value) -> Evaluation { let mut ctx = ValidationContext::new(); - let root = self - .root - .evaluate_instance(instance, &LazyLocation::new(), &mut ctx); + let evaluation_path = LazyRefPath::new(); + let root = + self.root + .evaluate_instance(instance, &LazyLocation::new(), &evaluation_path, &mut ctx); Evaluation::new(root) } /// The [`Draft`] which was used to build this validator. @@ -322,13 +553,14 @@ impl Validator { #[cfg(test)] mod tests { + use super::LazyEvaluationPath; use crate::{ error::ValidationError, keywords::custom::Keyword, paths::{LazyLocation, Location}, thread::ThreadBound, types::JsonType, - Validator, + ValidationContext, Validator, }; use fancy_regex::Regex; use num_cmp::NumCmp; @@ -403,13 +635,15 @@ mod tests { fn validate<'i>( &self, instance: &'i Value, - location: &LazyLocation, + instance_path: &LazyLocation, + ctx: &mut ValidationContext, + schema_path: &Location, ) -> Result<(), ValidationError<'i>> { for key in instance.as_object().unwrap().keys() { if !key.is_ascii() { - return Err(ValidationError::custom( - Location::new(), - location.into(), + return Err(ctx.custom_error( + schema_path, + instance_path, instance, "Key is not ASCII", )); @@ -438,8 +672,9 @@ mod tests { Ok(Box::new(CustomObjectValidator)) } else { Err(ValidationError::constant_string( - Location::new(), + path.clone(), path, + Location::new(), schema, EXPECTED, )) @@ -486,21 +721,24 @@ mod tests { limit: f64, limit_val: Value, with_currency_format: bool, - location: Location, } impl Keyword for CustomMinimumValidator { fn validate<'i>( &self, instance: &'i Value, - location: &LazyLocation, + instance_path: &LazyLocation, + _ctx: &mut ValidationContext, + schema_path: &Location, ) -> Result<(), ValidationError<'i>> { if self.is_valid(instance) { Ok(()) } else { Err(ValidationError::minimum( - self.location.clone(), - location.into(), + schema_path.clone(), + // Custom keywords are leaf validators - evaluation path == schema path + LazyEvaluationPath::from(schema_path.clone()), + instance_path.into(), instance, self.limit_val.clone(), )) @@ -548,9 +786,9 @@ mod tests { limit.as_f64().expect("Always valid") } else { return Err(ValidationError::single_type_error( - // There is no metaschema definition for a custom keyword, hence empty `schema` pointer - Location::new(), + location.clone(), location, + Location::new(), schema, JsonType::Number, )); @@ -562,7 +800,6 @@ mod tests { limit, limit_val: schema.clone(), with_currency_format, - location, })) } diff --git a/crates/jsonschema/tests/output-extra/v1-extra/content/dynamic_ref_chain.json b/crates/jsonschema/tests/output-extra/v1-extra/content/dynamic_ref_chain.json index 1ef84c2e..aa11bad8 100644 --- a/crates/jsonschema/tests/output-extra/v1-extra/content/dynamic_ref_chain.json +++ b/crates/jsonschema/tests/output-extra/v1-extra/content/dynamic_ref_chain.json @@ -58,13 +58,13 @@ { "valid": false, "evaluationPath": "/properties/child/$dynamicRef", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/child/$dynamicRef", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#", "instanceLocation": "/child" }, { "valid": false, "evaluationPath": "/properties/child/$dynamicRef/properties", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/child/$dynamicRef/properties", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties", "instanceLocation": "/child", "droppedAnnotations": [ "value" @@ -73,13 +73,13 @@ { "valid": false, "evaluationPath": "/properties/child/$dynamicRef/properties/value", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/child/$dynamicRef/properties/value", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/value", "instanceLocation": "/child/value" }, { "valid": false, "evaluationPath": "/properties/child/$dynamicRef/properties/value/type", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/child/$dynamicRef/properties/value/type", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/value/type", "instanceLocation": "/child/value", "errors": { "type": "\"boom\" is not of type \"integer\"" @@ -88,13 +88,13 @@ { "valid": true, "evaluationPath": "/properties/child/$dynamicRef/required", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/child/$dynamicRef/required", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/required", "instanceLocation": "/child" }, { "valid": true, "evaluationPath": "/properties/child/$dynamicRef/type", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/child/$dynamicRef/type", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/type", "instanceLocation": "/child" }, { @@ -151,13 +151,13 @@ { "valid": false, "evaluationPath": "/properties/child/$dynamicRef", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/child/$dynamicRef", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#", "instanceLocation": "/child", "details": [ { "valid": false, "evaluationPath": "/properties/child/$dynamicRef/properties", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/child/$dynamicRef/properties", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties", "instanceLocation": "/child", "droppedAnnotations": [ "value" @@ -166,13 +166,13 @@ { "valid": false, "evaluationPath": "/properties/child/$dynamicRef/properties/value", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/child/$dynamicRef/properties/value", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/value", "instanceLocation": "/child/value", "details": [ { "valid": false, "evaluationPath": "/properties/child/$dynamicRef/properties/value/type", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/child/$dynamicRef/properties/value/type", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/value/type", "instanceLocation": "/child/value", "errors": { "type": "\"boom\" is not of type \"integer\"" @@ -185,13 +185,13 @@ { "valid": true, "evaluationPath": "/properties/child/$dynamicRef/required", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/child/$dynamicRef/required", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/required", "instanceLocation": "/child" }, { "valid": true, "evaluationPath": "/properties/child/$dynamicRef/type", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/properties/child/$dynamicRef/type", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/dynamic_ref_chain/0#/type", "instanceLocation": "/child" } ] diff --git a/crates/jsonschema/tests/output-extra/v1-extra/content/list-hierarchical.json b/crates/jsonschema/tests/output-extra/v1-extra/content/list-hierarchical.json index 092b87ef..68b0c3e0 100644 --- a/crates/jsonschema/tests/output-extra/v1-extra/content/list-hierarchical.json +++ b/crates/jsonschema/tests/output-extra/v1-extra/content/list-hierarchical.json @@ -239,13 +239,13 @@ { "valid": false, "evaluationPath": "/properties/multi/allOf/0/$ref", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/properties/multi/allOf/0/$ref", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/$defs/integer", "instanceLocation": "/multi" }, { "valid": false, "evaluationPath": "/properties/multi/allOf/0/$ref/type", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/properties/multi/allOf/0/$ref/type", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/$defs/integer/type", "instanceLocation": "/multi", "errors": { "type": "3.5 is not of type \"integer\"" @@ -260,13 +260,13 @@ { "valid": false, "evaluationPath": "/properties/multi/allOf/1/$ref", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/properties/multi/allOf/1/$ref", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/$defs/minimum", "instanceLocation": "/multi" }, { "valid": false, "evaluationPath": "/properties/multi/allOf/1/$ref/minimum", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/properties/multi/allOf/1/$ref/minimum", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/$defs/minimum/minimum", "instanceLocation": "/multi", "errors": { "minimum": "3.5 is less than the minimum of 5" @@ -319,13 +319,13 @@ { "valid": false, "evaluationPath": "/properties/multi/allOf/0/$ref", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/properties/multi/allOf/0/$ref", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/$defs/integer", "instanceLocation": "/multi", "details": [ { "valid": false, "evaluationPath": "/properties/multi/allOf/0/$ref/type", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/properties/multi/allOf/0/$ref/type", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/$defs/integer/type", "instanceLocation": "/multi", "errors": { "type": "3.5 is not of type \"integer\"" @@ -344,13 +344,13 @@ { "valid": false, "evaluationPath": "/properties/multi/allOf/1/$ref", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/properties/multi/allOf/1/$ref", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/$defs/minimum", "instanceLocation": "/multi", "details": [ { "valid": false, "evaluationPath": "/properties/multi/allOf/1/$ref/minimum", - "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/properties/multi/allOf/1/$ref/minimum", + "schemaLocation": "https://json-schema.org/tests/content/v1-extra/list-hierarchical/0#/$defs/minimum/minimum", "instanceLocation": "/multi", "errors": { "minimum": "3.5 is less than the minimum of 5"