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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 81 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -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<String, Value>,
value: &'a Value,
schema_path: Location,
) -> Result<Box<dyn Keyword>, 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
Expand Down
8 changes: 8 additions & 0 deletions crates/jsonschema-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions crates/jsonschema-py/python/jsonschema_rs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions crates/jsonschema-py/python/jsonschema_rs/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
38 changes: 32 additions & 6 deletions crates/jsonschema-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ struct ValidationErrorArgs {
verbose_message: String,
schema_path: Py<PyList>,
instance_path: Py<PyList>,
evaluation_path: Py<PyList>,
kind: ValidationErrorKind,
instance: Py<PyAny>,
}
Expand All @@ -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,
))?;
Expand Down Expand Up @@ -410,15 +412,23 @@ 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 {
message,
verbose_message,
schema_path,
instance_path,
evaluation_path,
kind,
instance,
},
Expand Down Expand Up @@ -476,8 +486,15 @@ fn convert_validation_context(
let mut py_errors: Vec<Py<PyAny>> = 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,
Expand All @@ -486,6 +503,7 @@ fn convert_validation_context(
verbose_message,
schema_path,
instance_path,
evaluation_path,
kind,
instance,
},
Expand Down Expand Up @@ -523,6 +541,7 @@ fn into_validation_error_args(
String,
Py<PyList>,
Py<PyList>,
Py<PyList>,
ValidationErrorKind,
Py<PyAny>,
)> {
Expand All @@ -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::<PyAny>::try_from)
Expand All @@ -549,13 +568,19 @@ fn into_validation_error_args(
.map(into_path)
.collect::<Result<Vec<_>, _>>()?;
let instance_path = PyList::new(py, elements)?.unbind();
let elements = evaluation_path
.into_iter()
.map(into_path)
.collect::<Result<Vec<_>, _>>()?;
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((
message,
verbose_message,
schema_path,
instance_path,
evaluation_path,
kind,
instance,
))
Expand All @@ -565,7 +590,7 @@ fn into_py_err(
error: jsonschema::ValidationError<'_>,
mask: Option<&str>,
) -> PyResult<PyErr> {
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,
Expand All @@ -574,6 +599,7 @@ fn into_py_err(
verbose_message,
schema_path,
instance_path,
evaluation_path,
kind,
instance,
},
Expand Down
4 changes: 4 additions & 0 deletions crates/jsonschema-py/tests-py/test_jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ def test_validation_error_kinds(schema, instance, kind, attrs):
"",
["anyOf", 0, "type"],
[],
["anyOf", 0, "type"],
ValidationErrorKind.Type(["string"]),
True,
)
Expand All @@ -211,6 +212,7 @@ def test_validation_error_kinds(schema, instance, kind, attrs):
"",
["anyOf", 1, "type"],
[],
["anyOf", 1, "type"],
ValidationErrorKind.Type(["number"]),
True,
)
Expand All @@ -228,6 +230,7 @@ def test_validation_error_kinds(schema, instance, kind, attrs):
"",
["oneOf", 0, "type"],
[],
["oneOf", 0, "type"],
ValidationErrorKind.Type(["number"]),
"1",
)
Expand All @@ -238,6 +241,7 @@ def test_validation_error_kinds(schema, instance, kind, attrs):
"",
["oneOf", 1, "type"],
[],
["oneOf", 1, "type"],
ValidationErrorKind.Type(["number"]),
"1",
)
Expand Down
2 changes: 1 addition & 1 deletion crates/jsonschema-referencing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
23 changes: 14 additions & 9 deletions crates/jsonschema/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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),
))
}
}
}
Loading
Loading