diff --git a/crates/jsonschema-py/CHANGELOG.md b/crates/jsonschema-py/CHANGELOG.md index 624f2dc8..ea19061a 100644 --- a/crates/jsonschema-py/CHANGELOG.md +++ b/crates/jsonschema-py/CHANGELOG.md @@ -6,6 +6,7 @@ - Custom retrievers for external references. [#372](https://github.com/Stranger6667/jsonschema/issues/372) - Added the `mask` argument to validators for hiding sensitive data in error messages. [#434](https://github.com/Stranger6667/jsonschema/issues/434) +- Added `ValidationError.kind` and `ValidationError.instance` attributes. [#650](https://github.com/Stranger6667/jsonschema/issues/650) ### Changed diff --git a/crates/jsonschema-py/Cargo.toml b/crates/jsonschema-py/Cargo.toml index b923d800..53f9474d 100644 --- a/crates/jsonschema-py/Cargo.toml +++ b/crates/jsonschema-py/Cargo.toml @@ -24,4 +24,5 @@ jsonschema = { path = "../jsonschema/" } serde.workspace = true serde_json.workspace = true pyo3 = { version = "0.23.3", features = ["extension-module"] } +pythonize = "0.23" pyo3-built = "0.5" diff --git a/crates/jsonschema-py/src/lib.rs b/crates/jsonschema-py/src/lib.rs index 95a96253..7da0f24e 100644 --- a/crates/jsonschema-py/src/lib.rs +++ b/crates/jsonschema-py/src/lib.rs @@ -39,6 +39,10 @@ struct ValidationError { schema_path: Py, #[pyo3(get)] instance_path: Py, + #[pyo3(get)] + kind: ValidationErrorKind, + #[pyo3(get)] + instance: PyObject, } #[pymethods] @@ -49,12 +53,16 @@ impl ValidationError { long_message: String, schema_path: Py, instance_path: Py, + kind: ValidationErrorKind, + instance: PyObject, ) -> Self { ValidationError { message, verbose_message: long_message, schema_path, instance_path, + kind, + instance, } } fn __str__(&self) -> String { @@ -65,6 +73,134 @@ impl ValidationError { } } +#[pyclass(eq, eq_int)] +#[derive(Debug, PartialEq, Clone)] +enum ValidationErrorKind { + AdditionalItems, + AdditionalProperties, + AnyOf, + BacktrackLimitExceeded, + Constant, + Contains, + ContentEncoding, + ContentMediaType, + Custom, + Enum, + ExclusiveMaximum, + ExclusiveMinimum, + FalseSchema, + Format, + FromUtf8, + MaxItems, + Maximum, + MaxLength, + MaxProperties, + MinItems, + Minimum, + MinLength, + MinProperties, + MultipleOf, + Not, + OneOfMultipleValid, + OneOfNotValid, + Pattern, + PropertyNames, + Required, + Type, + UnevaluatedItems, + UnevaluatedProperties, + UniqueItems, + Referencing, +} + +impl From for ValidationErrorKind { + fn from(kind: jsonschema::error::ValidationErrorKind) -> Self { + match kind { + jsonschema::error::ValidationErrorKind::AdditionalItems { .. } => { + ValidationErrorKind::AdditionalItems + } + jsonschema::error::ValidationErrorKind::AdditionalProperties { .. } => { + ValidationErrorKind::AdditionalProperties + } + jsonschema::error::ValidationErrorKind::AnyOf => ValidationErrorKind::AnyOf, + jsonschema::error::ValidationErrorKind::BacktrackLimitExceeded { .. } => { + ValidationErrorKind::BacktrackLimitExceeded + } + jsonschema::error::ValidationErrorKind::Constant { .. } => { + ValidationErrorKind::Constant + } + jsonschema::error::ValidationErrorKind::Contains => ValidationErrorKind::Contains, + jsonschema::error::ValidationErrorKind::ContentEncoding { .. } => { + ValidationErrorKind::ContentEncoding + } + jsonschema::error::ValidationErrorKind::ContentMediaType { .. } => { + ValidationErrorKind::ContentMediaType + } + jsonschema::error::ValidationErrorKind::Custom { .. } => ValidationErrorKind::Custom, + jsonschema::error::ValidationErrorKind::Enum { .. } => ValidationErrorKind::Enum, + jsonschema::error::ValidationErrorKind::ExclusiveMaximum { .. } => { + ValidationErrorKind::ExclusiveMaximum + } + jsonschema::error::ValidationErrorKind::ExclusiveMinimum { .. } => { + ValidationErrorKind::ExclusiveMinimum + } + jsonschema::error::ValidationErrorKind::FalseSchema => ValidationErrorKind::FalseSchema, + jsonschema::error::ValidationErrorKind::Format { .. } => ValidationErrorKind::Format, + jsonschema::error::ValidationErrorKind::FromUtf8 { .. } => { + ValidationErrorKind::FromUtf8 + } + jsonschema::error::ValidationErrorKind::MaxItems { .. } => { + ValidationErrorKind::MaxItems + } + jsonschema::error::ValidationErrorKind::Maximum { .. } => ValidationErrorKind::Maximum, + jsonschema::error::ValidationErrorKind::MaxLength { .. } => { + ValidationErrorKind::MaxLength + } + jsonschema::error::ValidationErrorKind::MaxProperties { .. } => { + ValidationErrorKind::MaxProperties + } + jsonschema::error::ValidationErrorKind::MinItems { .. } => { + ValidationErrorKind::MinItems + } + jsonschema::error::ValidationErrorKind::Minimum { .. } => ValidationErrorKind::Minimum, + jsonschema::error::ValidationErrorKind::MinLength { .. } => { + ValidationErrorKind::MinLength + } + jsonschema::error::ValidationErrorKind::MinProperties { .. } => { + ValidationErrorKind::MinProperties + } + jsonschema::error::ValidationErrorKind::MultipleOf { .. } => { + ValidationErrorKind::MultipleOf + } + jsonschema::error::ValidationErrorKind::Not { .. } => ValidationErrorKind::Not, + jsonschema::error::ValidationErrorKind::OneOfMultipleValid => { + ValidationErrorKind::OneOfMultipleValid + } + jsonschema::error::ValidationErrorKind::OneOfNotValid => { + ValidationErrorKind::OneOfNotValid + } + jsonschema::error::ValidationErrorKind::Pattern { .. } => ValidationErrorKind::Pattern, + jsonschema::error::ValidationErrorKind::PropertyNames { .. } => { + ValidationErrorKind::PropertyNames + } + jsonschema::error::ValidationErrorKind::Required { .. } => { + ValidationErrorKind::Required + } + jsonschema::error::ValidationErrorKind::Type { .. } => ValidationErrorKind::Type, + jsonschema::error::ValidationErrorKind::UnevaluatedItems { .. } => { + ValidationErrorKind::UnevaluatedItems + } + jsonschema::error::ValidationErrorKind::UnevaluatedProperties { .. } => { + ValidationErrorKind::UnevaluatedProperties + } + jsonschema::error::ValidationErrorKind::UniqueItems => ValidationErrorKind::UniqueItems, + jsonschema::error::ValidationErrorKind::Referencing(_) => { + ValidationErrorKind::Referencing + } + } + } +} + #[pyclass] struct ValidationErrorIter { iter: std::vec::IntoIter, @@ -115,9 +251,18 @@ fn into_py_err( .map(into_path) .collect::, _>>()?; let instance_path = PyList::new(py, elements)?.unbind(); + let kind: ValidationErrorKind = error.kind.into(); + let instance = pythonize::pythonize(py, error.instance.as_ref())?.unbind(); Ok(PyErr::from_type( pyerror_type, - (message, verbose_message, schema_path, instance_path), + ( + message, + verbose_message, + schema_path, + instance_path, + kind, + instance, + ), )) } @@ -831,6 +976,7 @@ fn jsonschema_rs(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add("ValidationError", py.get_type::())?; + module.add("ValidationErrorKind", py.get_type::())?; module.add("Draft4", DRAFT4)?; module.add("Draft6", DRAFT6)?; module.add("Draft7", DRAFT7)?; diff --git a/crates/jsonschema-py/tests-py/test_jsonschema.py b/crates/jsonschema-py/tests-py/test_jsonschema.py index 33ec79f2..73df7fa6 100644 --- a/crates/jsonschema-py/tests-py/test_jsonschema.py +++ b/crates/jsonschema-py/tests-py/test_jsonschema.py @@ -11,6 +11,7 @@ from jsonschema_rs import ( ValidationError, + ValidationErrorKind, is_valid, iter_errors, validate, @@ -73,8 +74,10 @@ def test_repr(): ), ) def test_validate(func): - with pytest.raises(ValidationError, match="2 is less than the minimum of 5"): + with pytest.raises(ValidationError, match="2 is less than the minimum of 5") as exc: func(2) + assert exc.value.kind == ValidationErrorKind.Minimum + assert exc.value.instance == 2 def test_from_str_error(): @@ -132,6 +135,8 @@ def test_paths(): assert exc.value.schema_path == ["prefixItems", 0, "type"] assert exc.value.instance_path == [0] assert exc.value.message == '1 is not of type "string"' + assert exc.value.kind == ValidationErrorKind.Type + assert exc.value.instance == 1 @given(minimum=st.integers().map(abs)) @@ -178,6 +183,24 @@ def test_error_message(): On instance["foo"]: null""" ) + assert exc.kind == ValidationErrorKind.Type + assert exc.instance is None + + +def test_error_instance(): + instance = {"a": [42]} + try: + validate({"type": "array"}, instance) + pytest.fail("Validation error should happen") + except ValidationError as exc: + assert exc.kind == ValidationErrorKind.Type + assert exc.instance == instance + try: + validate({"properties": {"a": {"type": "object"}}}, instance) + pytest.fail("Validation error should happen") + except ValidationError as exc: + assert exc.kind == ValidationErrorKind.Type + assert exc.instance == instance["a"] SCHEMA = {"properties": {"foo": {"type": "integer"}, "bar": {"type": "string"}}}