Skip to content

Commit e06d3c1

Browse files
committed
fix: Evaluation paths to exclude $ref / $dynamicRef / $recursiveRef
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
1 parent 9748dc1 commit e06d3c1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+2747
-722
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- `ValidationError::evaluation_path()` returning the path including `$ref` traversals.
8+
- `ValidationContext::custom_error()` for creating validation errors with correct `evaluation_path`.
9+
- `ValidationError::schema()` for creating errors in keyword factory functions.
10+
11+
### Changed
12+
13+
- **BREAKING**: `Keyword::validate` now receives `ValidationContext` and `schema_path` parameters.
14+
- **BREAKING**: `ValidationError::custom` is now internal. Use `ctx.custom_error()` or `ValidationError::schema()` instead.
15+
16+
### Fixed
17+
18+
- `schemaLocation` in evaluation output now excludes `$ref`/`$dynamicRef`/`$recursiveRef` per JSON Schema spec.
19+
520
## [0.37.4] - 2025-11-30
621

722
### Fixed

MIGRATION.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,86 @@
11
# Migration Guide
22

3+
## Upgrading from 0.37.x to 0.38.0
4+
5+
### Custom keyword API changes
6+
7+
The custom keyword API now tracks `$ref` traversals to provide correct `evaluation_path` values in errors.
8+
When a custom keyword is reached via `$ref`, the error's `evaluation_path` will include the `$ref` location,
9+
while `schema_path` remains the canonical schema location.
10+
11+
**`Keyword::validate` signature:**
12+
13+
```rust
14+
// Old (0.37.x)
15+
fn validate<'i>(
16+
&self,
17+
instance: &'i Value,
18+
location: &LazyLocation,
19+
) -> Result<(), ValidationError<'i>>;
20+
21+
// New (0.38.0)
22+
fn validate<'i>(
23+
&self,
24+
instance: &'i Value,
25+
instance_path: &LazyLocation,
26+
ctx: &mut ValidationContext,
27+
schema_path: &Location,
28+
) -> Result<(), ValidationError<'i>>;
29+
```
30+
31+
**Creating errors:**
32+
33+
```rust
34+
// Old (0.37.x)
35+
ValidationError::custom(schema_path, instance_path, instance, message)
36+
37+
// New (0.38.0) - for validation errors
38+
ctx.custom_error(schema_path, instance_path, instance, message)
39+
40+
// New (0.38.0) - for factory errors (invalid schema values)
41+
ValidationError::schema(schema_path, schema_value, message)
42+
```
43+
44+
**Updated implementation example:**
45+
46+
```rust
47+
use jsonschema::{Keyword, ValidationContext, ValidationError, paths::{LazyLocation, Location}};
48+
use serde_json::{Map, Value};
49+
50+
struct MyValidator;
51+
52+
impl Keyword for MyValidator {
53+
fn validate<'i>(
54+
&self,
55+
instance: &'i Value,
56+
instance_path: &LazyLocation,
57+
ctx: &mut ValidationContext,
58+
schema_path: &Location,
59+
) -> Result<(), ValidationError<'i>> {
60+
if !instance.is_string() {
61+
return Err(ctx.custom_error(schema_path, instance_path, instance, "expected a string"));
62+
}
63+
Ok(())
64+
}
65+
66+
fn is_valid(&self, instance: &Value) -> bool {
67+
instance.is_string()
68+
}
69+
}
70+
71+
fn my_keyword_factory<'a>(
72+
_parent: &'a Map<String, Value>,
73+
value: &'a Value,
74+
schema_path: Location,
75+
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
76+
if value.as_bool() == Some(true) {
77+
Ok(Box::new(MyValidator))
78+
} else {
79+
Err(ValidationError::schema(schema_path, value, "expected true"))
80+
}
81+
}
82+
```
83+
384
## Upgrading from 0.36.x to 0.37.0
485

586
### `ValidationError` is now opaque

crates/jsonschema-py/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- `ValidationError.evaluation_path` attribute returning the path including `$ref` traversals.
8+
9+
### Fixed
10+
11+
- `schemaLocation` in evaluation output now excludes `$ref`/`$dynamicRef`/`$recursiveRef` per JSON Schema spec.
12+
513
## [0.37.4] - 2025-11-30
614

715
### Fixed

crates/jsonschema-py/python/jsonschema_rs/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class ValidationError(ValueError):
3636
verbose_message: str
3737
schema_path: list[str | int]
3838
instance_path: list[str | int]
39+
evaluation_path: list[str | int]
3940
kind: ValidationErrorKind
4041
instance: Any
4142

@@ -45,6 +46,7 @@ def __init__(
4546
verbose_message: str,
4647
schema_path: list[str | int],
4748
instance_path: list[str | int],
49+
evaluation_path: list[str | int],
4850
kind: ValidationErrorKind,
4951
instance: Any,
5052
) -> None:
@@ -53,6 +55,7 @@ def __init__(
5355
self.verbose_message = verbose_message
5456
self.schema_path = schema_path
5557
self.instance_path = instance_path
58+
self.evaluation_path = evaluation_path
5659
self.kind = kind
5760
self.instance = instance
5861

crates/jsonschema-py/python/jsonschema_rs/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ class ValidationError(ValueError):
255255
verbose_message: str
256256
schema_path: list[str | int]
257257
instance_path: list[str | int]
258+
evaluation_path: list[str | int]
258259
kind: ValidationErrorKind
259260
instance: JSONType
260261

crates/jsonschema-py/src/lib.rs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ struct ValidationErrorArgs {
216216
verbose_message: String,
217217
schema_path: Py<PyList>,
218218
instance_path: Py<PyList>,
219+
evaluation_path: Py<PyList>,
219220
kind: ValidationErrorKind,
220221
instance: Py<PyAny>,
221222
}
@@ -231,6 +232,7 @@ fn create_validation_error_object(
231232
args.verbose_message,
232233
args.schema_path,
233234
args.instance_path,
235+
args.evaluation_path,
234236
kind_obj,
235237
args.instance,
236238
))?;
@@ -410,15 +412,23 @@ impl ValidationErrorKind {
410412
jsonschema::error::ValidationErrorKind::PropertyNames { error } => {
411413
ValidationErrorKind::PropertyNames {
412414
error: {
413-
let (message, verbose_message, schema_path, instance_path, kind, instance) =
414-
into_validation_error_args(py, *error, mask)?;
415+
let (
416+
message,
417+
verbose_message,
418+
schema_path,
419+
instance_path,
420+
evaluation_path,
421+
kind,
422+
instance,
423+
) = into_validation_error_args(py, *error, mask)?;
415424
create_validation_error_object(
416425
py,
417426
ValidationErrorArgs {
418427
message,
419428
verbose_message,
420429
schema_path,
421430
instance_path,
431+
evaluation_path,
422432
kind,
423433
instance,
424434
},
@@ -476,8 +486,15 @@ fn convert_validation_context(
476486
let mut py_errors: Vec<Py<PyAny>> = Vec::with_capacity(errors.len());
477487

478488
for error in errors {
479-
let (message, verbose_message, schema_path, instance_path, kind, instance) =
480-
into_validation_error_args(py, error, mask)?;
489+
let (
490+
message,
491+
verbose_message,
492+
schema_path,
493+
instance_path,
494+
evaluation_path,
495+
kind,
496+
instance,
497+
) = into_validation_error_args(py, error, mask)?;
481498

482499
py_errors.push(create_validation_error_object(
483500
py,
@@ -486,6 +503,7 @@ fn convert_validation_context(
486503
verbose_message,
487504
schema_path,
488505
instance_path,
506+
evaluation_path,
489507
kind,
490508
instance,
491509
},
@@ -523,6 +541,7 @@ fn into_validation_error_args(
523541
String,
524542
Py<PyList>,
525543
Py<PyList>,
544+
Py<PyList>,
526545
ValidationErrorKind,
527546
Py<PyAny>,
528547
)> {
@@ -532,7 +551,7 @@ fn into_validation_error_args(
532551
error.to_string()
533552
};
534553
let verbose_message = to_error_message(&error, message.clone(), mask);
535-
let (instance, kind, instance_path, schema_path) = error.into_parts();
554+
let (instance, kind, instance_path, schema_path, evaluation_path) = error.into_parts();
536555
let into_path = |segment: LocationSegment<'_>| match segment {
537556
LocationSegment::Property(property) => {
538557
property.into_pyobject(py).and_then(Py::<PyAny>::try_from)
@@ -549,13 +568,19 @@ fn into_validation_error_args(
549568
.map(into_path)
550569
.collect::<Result<Vec<_>, _>>()?;
551570
let instance_path = PyList::new(py, elements)?.unbind();
571+
let elements = evaluation_path
572+
.into_iter()
573+
.map(into_path)
574+
.collect::<Result<Vec<_>, _>>()?;
575+
let evaluation_path = PyList::new(py, elements)?.unbind();
552576
let kind = ValidationErrorKind::try_new(py, kind, mask)?;
553577
let instance = value_to_python(py, instance.as_ref())?;
554578
Ok((
555579
message,
556580
verbose_message,
557581
schema_path,
558582
instance_path,
583+
evaluation_path,
559584
kind,
560585
instance,
561586
))
@@ -565,7 +590,7 @@ fn into_py_err(
565590
error: jsonschema::ValidationError<'_>,
566591
mask: Option<&str>,
567592
) -> PyResult<PyErr> {
568-
let (message, verbose_message, schema_path, instance_path, kind, instance) =
593+
let (message, verbose_message, schema_path, instance_path, evaluation_path, kind, instance) =
569594
into_validation_error_args(py, error, mask)?;
570595
validation_error_pyerr(
571596
py,
@@ -574,6 +599,7 @@ fn into_py_err(
574599
verbose_message,
575600
schema_path,
576601
instance_path,
602+
evaluation_path,
577603
kind,
578604
instance,
579605
},

crates/jsonschema-py/tests-py/test_jsonschema.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ def test_validation_error_kinds(schema, instance, kind, attrs):
201201
"",
202202
["anyOf", 0, "type"],
203203
[],
204+
["anyOf", 0, "type"],
204205
ValidationErrorKind.Type(["string"]),
205206
True,
206207
)
@@ -211,6 +212,7 @@ def test_validation_error_kinds(schema, instance, kind, attrs):
211212
"",
212213
["anyOf", 1, "type"],
213214
[],
215+
["anyOf", 1, "type"],
214216
ValidationErrorKind.Type(["number"]),
215217
True,
216218
)
@@ -228,6 +230,7 @@ def test_validation_error_kinds(schema, instance, kind, attrs):
228230
"",
229231
["oneOf", 0, "type"],
230232
[],
233+
["oneOf", 0, "type"],
231234
ValidationErrorKind.Type(["number"]),
232235
"1",
233236
)
@@ -238,6 +241,7 @@ def test_validation_error_kinds(schema, instance, kind, attrs):
238241
"",
239242
["oneOf", 1, "type"],
240243
[],
244+
["oneOf", 1, "type"],
241245
ValidationErrorKind.Type(["number"]),
242246
"1",
243247
)

crates/jsonschema-referencing/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ mod vocabularies;
1717

1818
pub(crate) use anchors::Anchor;
1919
pub use error::{Error, UriError};
20-
pub use fluent_uri::{Iri, IriRef, Uri, UriRef};
20+
pub use fluent_uri::{pct_enc::EStr, Iri, IriRef, Uri, UriRef};
2121
pub use list::List;
2222
pub use registry::{parse_index, pointer, Registry, RegistryOptions, SPECIFICATIONS};
2323
pub use resolver::{Resolved, Resolver};

crates/jsonschema/src/compiler.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,8 @@ fn compile_without_cache<'a>(
890890
// Check if this keyword is overridden, then check the standard definitions
891891
if let Some(factory) = ctx.get_keyword_factory(keyword) {
892892
let path = ctx.location().join(keyword);
893-
let validator = CustomKeyword::new(factory.init(schema, value, path)?);
893+
let validator =
894+
CustomKeyword::new(factory.init(schema, value, path.clone())?, path);
894895
let validator: BoxedValidator = Box::new(validator);
895896
validators.push((Keyword::custom(keyword), validator));
896897
} else if let Some((keyword, validator)) = keywords::get_for_draft(ctx, keyword)
@@ -909,13 +910,17 @@ fn compile_without_cache<'a>(
909910
};
910911
Ok(SchemaNode::from_keywords(ctx, validators, annotations))
911912
}
912-
_ => Err(ValidationError::multiple_type_error(
913-
Location::new(),
914-
ctx.location().clone(),
915-
resource.contents(),
916-
JsonTypeSet::empty()
917-
.insert(JsonType::Boolean)
918-
.insert(JsonType::Object),
919-
)),
913+
_ => {
914+
let location = ctx.location().clone();
915+
Err(ValidationError::multiple_type_error(
916+
location.clone(),
917+
location,
918+
Location::new(),
919+
resource.contents(),
920+
JsonTypeSet::empty()
921+
.insert(JsonType::Boolean)
922+
.insert(JsonType::Object),
923+
))
924+
}
920925
}
921926
}

0 commit comments

Comments
 (0)