Skip to content

Commit 91d8e3f

Browse files
committed
feat(python): Added ValidationError.kind and ValidationError.instance attributes
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
1 parent b3d2d6c commit 91d8e3f

File tree

5 files changed

+177
-2
lines changed

5 files changed

+177
-2
lines changed

crates/jsonschema-py/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Custom retrievers for external references. [#372](https://github.com/Stranger6667/jsonschema/issues/372)
88
- Added the `mask` argument to validators for hiding sensitive data in error messages. [#434](https://github.com/Stranger6667/jsonschema/issues/434)
9+
- Added `ValidationError.kind` and `ValidationError.instance` attributes. [#650](https://github.com/Stranger6667/jsonschema/issues/650)
910

1011
### Changed
1112

crates/jsonschema-py/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ jsonschema = { path = "../jsonschema/" }
2424
serde.workspace = true
2525
serde_json.workspace = true
2626
pyo3 = { version = "0.23.3", features = ["extension-module"] }
27+
pythonize = "0.23"
2728
pyo3-built = "0.5"

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@ def iter_errors(
3232
ignore_unknown_formats: bool = True,
3333
) -> Iterator[ValidationError]: ...
3434

35+
class ValidationErrorKind: ...
36+
3537
class ValidationError(ValueError):
3638
message: str
3739
schema_path: list[str | int]
3840
instance_path: list[str | int]
41+
kind: ValidationErrorKind
42+
instance: list | dict | str | int | float | bool | None
3943

4044
Draft4: int
4145
Draft6: int

crates/jsonschema-py/src/lib.rs

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ struct ValidationError {
3939
schema_path: Py<PyList>,
4040
#[pyo3(get)]
4141
instance_path: Py<PyList>,
42+
#[pyo3(get)]
43+
kind: ValidationErrorKind,
44+
#[pyo3(get)]
45+
instance: PyObject,
4246
}
4347

4448
#[pymethods]
@@ -49,12 +53,16 @@ impl ValidationError {
4953
long_message: String,
5054
schema_path: Py<PyList>,
5155
instance_path: Py<PyList>,
56+
kind: ValidationErrorKind,
57+
instance: PyObject,
5258
) -> Self {
5359
ValidationError {
5460
message,
5561
verbose_message: long_message,
5662
schema_path,
5763
instance_path,
64+
kind,
65+
instance,
5866
}
5967
}
6068
fn __str__(&self) -> String {
@@ -65,6 +73,134 @@ impl ValidationError {
6573
}
6674
}
6775

76+
#[pyclass(eq, eq_int)]
77+
#[derive(Debug, PartialEq, Clone)]
78+
enum ValidationErrorKind {
79+
AdditionalItems,
80+
AdditionalProperties,
81+
AnyOf,
82+
BacktrackLimitExceeded,
83+
Constant,
84+
Contains,
85+
ContentEncoding,
86+
ContentMediaType,
87+
Custom,
88+
Enum,
89+
ExclusiveMaximum,
90+
ExclusiveMinimum,
91+
FalseSchema,
92+
Format,
93+
FromUtf8,
94+
MaxItems,
95+
Maximum,
96+
MaxLength,
97+
MaxProperties,
98+
MinItems,
99+
Minimum,
100+
MinLength,
101+
MinProperties,
102+
MultipleOf,
103+
Not,
104+
OneOfMultipleValid,
105+
OneOfNotValid,
106+
Pattern,
107+
PropertyNames,
108+
Required,
109+
Type,
110+
UnevaluatedItems,
111+
UnevaluatedProperties,
112+
UniqueItems,
113+
Referencing,
114+
}
115+
116+
impl From<jsonschema::error::ValidationErrorKind> for ValidationErrorKind {
117+
fn from(kind: jsonschema::error::ValidationErrorKind) -> Self {
118+
match kind {
119+
jsonschema::error::ValidationErrorKind::AdditionalItems { .. } => {
120+
ValidationErrorKind::AdditionalItems
121+
}
122+
jsonschema::error::ValidationErrorKind::AdditionalProperties { .. } => {
123+
ValidationErrorKind::AdditionalProperties
124+
}
125+
jsonschema::error::ValidationErrorKind::AnyOf => ValidationErrorKind::AnyOf,
126+
jsonschema::error::ValidationErrorKind::BacktrackLimitExceeded { .. } => {
127+
ValidationErrorKind::BacktrackLimitExceeded
128+
}
129+
jsonschema::error::ValidationErrorKind::Constant { .. } => {
130+
ValidationErrorKind::Constant
131+
}
132+
jsonschema::error::ValidationErrorKind::Contains => ValidationErrorKind::Contains,
133+
jsonschema::error::ValidationErrorKind::ContentEncoding { .. } => {
134+
ValidationErrorKind::ContentEncoding
135+
}
136+
jsonschema::error::ValidationErrorKind::ContentMediaType { .. } => {
137+
ValidationErrorKind::ContentMediaType
138+
}
139+
jsonschema::error::ValidationErrorKind::Custom { .. } => ValidationErrorKind::Custom,
140+
jsonschema::error::ValidationErrorKind::Enum { .. } => ValidationErrorKind::Enum,
141+
jsonschema::error::ValidationErrorKind::ExclusiveMaximum { .. } => {
142+
ValidationErrorKind::ExclusiveMaximum
143+
}
144+
jsonschema::error::ValidationErrorKind::ExclusiveMinimum { .. } => {
145+
ValidationErrorKind::ExclusiveMinimum
146+
}
147+
jsonschema::error::ValidationErrorKind::FalseSchema => ValidationErrorKind::FalseSchema,
148+
jsonschema::error::ValidationErrorKind::Format { .. } => ValidationErrorKind::Format,
149+
jsonschema::error::ValidationErrorKind::FromUtf8 { .. } => {
150+
ValidationErrorKind::FromUtf8
151+
}
152+
jsonschema::error::ValidationErrorKind::MaxItems { .. } => {
153+
ValidationErrorKind::MaxItems
154+
}
155+
jsonschema::error::ValidationErrorKind::Maximum { .. } => ValidationErrorKind::Maximum,
156+
jsonschema::error::ValidationErrorKind::MaxLength { .. } => {
157+
ValidationErrorKind::MaxLength
158+
}
159+
jsonschema::error::ValidationErrorKind::MaxProperties { .. } => {
160+
ValidationErrorKind::MaxProperties
161+
}
162+
jsonschema::error::ValidationErrorKind::MinItems { .. } => {
163+
ValidationErrorKind::MinItems
164+
}
165+
jsonschema::error::ValidationErrorKind::Minimum { .. } => ValidationErrorKind::Minimum,
166+
jsonschema::error::ValidationErrorKind::MinLength { .. } => {
167+
ValidationErrorKind::MinLength
168+
}
169+
jsonschema::error::ValidationErrorKind::MinProperties { .. } => {
170+
ValidationErrorKind::MinProperties
171+
}
172+
jsonschema::error::ValidationErrorKind::MultipleOf { .. } => {
173+
ValidationErrorKind::MultipleOf
174+
}
175+
jsonschema::error::ValidationErrorKind::Not { .. } => ValidationErrorKind::Not,
176+
jsonschema::error::ValidationErrorKind::OneOfMultipleValid => {
177+
ValidationErrorKind::OneOfMultipleValid
178+
}
179+
jsonschema::error::ValidationErrorKind::OneOfNotValid => {
180+
ValidationErrorKind::OneOfNotValid
181+
}
182+
jsonschema::error::ValidationErrorKind::Pattern { .. } => ValidationErrorKind::Pattern,
183+
jsonschema::error::ValidationErrorKind::PropertyNames { .. } => {
184+
ValidationErrorKind::PropertyNames
185+
}
186+
jsonschema::error::ValidationErrorKind::Required { .. } => {
187+
ValidationErrorKind::Required
188+
}
189+
jsonschema::error::ValidationErrorKind::Type { .. } => ValidationErrorKind::Type,
190+
jsonschema::error::ValidationErrorKind::UnevaluatedItems { .. } => {
191+
ValidationErrorKind::UnevaluatedItems
192+
}
193+
jsonschema::error::ValidationErrorKind::UnevaluatedProperties { .. } => {
194+
ValidationErrorKind::UnevaluatedProperties
195+
}
196+
jsonschema::error::ValidationErrorKind::UniqueItems => ValidationErrorKind::UniqueItems,
197+
jsonschema::error::ValidationErrorKind::Referencing(_) => {
198+
ValidationErrorKind::Referencing
199+
}
200+
}
201+
}
202+
}
203+
68204
#[pyclass]
69205
struct ValidationErrorIter {
70206
iter: std::vec::IntoIter<PyErr>,
@@ -115,9 +251,18 @@ fn into_py_err(
115251
.map(into_path)
116252
.collect::<Result<Vec<_>, _>>()?;
117253
let instance_path = PyList::new(py, elements)?.unbind();
254+
let kind: ValidationErrorKind = error.kind.into();
255+
let instance = pythonize::pythonize(py, error.instance.as_ref())?.unbind();
118256
Ok(PyErr::from_type(
119257
pyerror_type,
120-
(message, verbose_message, schema_path, instance_path),
258+
(
259+
message,
260+
verbose_message,
261+
schema_path,
262+
instance_path,
263+
kind,
264+
instance,
265+
),
121266
))
122267
}
123268

@@ -831,6 +976,7 @@ fn jsonschema_rs(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
831976
module.add_class::<Draft201909Validator>()?;
832977
module.add_class::<Draft202012Validator>()?;
833978
module.add("ValidationError", py.get_type::<ValidationError>())?;
979+
module.add("ValidationErrorKind", py.get_type::<ValidationErrorKind>())?;
834980
module.add("Draft4", DRAFT4)?;
835981
module.add("Draft6", DRAFT6)?;
836982
module.add("Draft7", DRAFT7)?;

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from jsonschema_rs import (
1313
ValidationError,
14+
ValidationErrorKind,
1415
is_valid,
1516
iter_errors,
1617
validate,
@@ -73,8 +74,10 @@ def test_repr():
7374
),
7475
)
7576
def test_validate(func):
76-
with pytest.raises(ValidationError, match="2 is less than the minimum of 5"):
77+
with pytest.raises(ValidationError, match="2 is less than the minimum of 5") as exc:
7778
func(2)
79+
assert exc.value.kind == ValidationErrorKind.Minimum
80+
assert exc.value.instance == 2
7881

7982

8083
def test_from_str_error():
@@ -132,6 +135,8 @@ def test_paths():
132135
assert exc.value.schema_path == ["prefixItems", 0, "type"]
133136
assert exc.value.instance_path == [0]
134137
assert exc.value.message == '1 is not of type "string"'
138+
assert exc.value.kind == ValidationErrorKind.Type
139+
assert exc.value.instance == 1
135140

136141

137142
@given(minimum=st.integers().map(abs))
@@ -178,6 +183,24 @@ def test_error_message():
178183
On instance["foo"]:
179184
null"""
180185
)
186+
assert exc.kind == ValidationErrorKind.Type
187+
assert exc.instance is None
188+
189+
190+
def test_error_instance():
191+
instance = {"a": [42]}
192+
try:
193+
validate({"type": "array"}, instance)
194+
pytest.fail("Validation error should happen")
195+
except ValidationError as exc:
196+
assert exc.kind == ValidationErrorKind.Type
197+
assert exc.instance == instance
198+
try:
199+
validate({"properties": {"a": {"type": "object"}}}, instance)
200+
pytest.fail("Validation error should happen")
201+
except ValidationError as exc:
202+
assert exc.kind == ValidationErrorKind.Type
203+
assert exc.instance == instance["a"]
181204

182205

183206
SCHEMA = {"properties": {"foo": {"type": "integer"}, "bar": {"type": "string"}}}

0 commit comments

Comments
 (0)