Source code for jsonfield_validation.validator
from collections import defaultdict
from typing import Any, Dict, Iterable, List, Optional
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.utils.deconstruct import deconstructible
from jsonschema import SchemaError
from jsonschema import ValidationError as SchemaValidationError
from jsonschema.validators import validator_for
# NOTE: The deconstructible decorator allows Django to serialize the
# validator instance, this plus the __eq__ method prevent new migrations
# being created on every makemigrations run.
# See https://docs.djangoproject.com/en/4.0/topics/migrations/#adding-a-deconstruct-method # noqa
[docs]@deconstructible
class JsonSchemaValidator:
"""
Django model field validator for use with JSONField.
Validates against a given JSON Schema.
"""
nested_item_delimiter = "."
def __init__(self, schema: Dict):
self.validator = validator_for(schema)(schema)
try:
self.validator.check_schema(schema)
except SchemaError as e:
raise ImproperlyConfigured("Not a valid JSON schema") from e
self.schema = schema
def __call__(self, value):
errors = self.check(value)
if errors is not None:
# Field.run_validators doesn't work with dict-based ValidationErrors,
# so we set error_list as the list of errors, but also set
# error_dict explicitly.
# See https://code.djangoproject.com/ticket/29318
ve = ValidationError(self._errordict_to_list(errors))
ve.error_dict = errors
raise ve
[docs] def check(self, value) -> Optional[Dict]:
"""
Check value against the schema without raising an exception.
If there are errors, they are returned as a dictionary. If the
value is a JSON object, errors are keyed by the path through to
the errant attribute.
"""
errors = []
for error in sorted(self.validator.iter_errors(value), key=str):
errors.append(error)
if errors:
return self._errors_to_dict(errors)
return None
@classmethod
def _errors_to_dict(
cls,
error_list: Iterable[SchemaValidationError],
) -> Dict[str, List[str]]:
"""
Convert a list of jsonschema ValidationError to a dictionary.
For an object with nested values, dict keys will be flattened
using the nested_item_delimiter. If an error is not related to
an individual attribute, it will be listed under the key
"__non_field_errors__".
The value will be a list of errors pertaining to that key.
"""
errors = defaultdict(list)
for ve in error_list:
key = cls.nested_item_delimiter.join(cls._stringify_path_elements(ve.path))
key = key or "__non_field_errors__"
errors[key].append(ve.message)
return errors
@classmethod
def _errordict_to_list(cls, errordict: Dict[str, List[str]]) -> List[str]:
"""
Flatten the error dict down to a list of strings by prepending
each error value with the error key.
"""
return [f"{k}: {v}" for k, v in errordict.items()]
@classmethod
def _stringify_path_elements(cls, path: Iterable[Any]) -> List[str]:
elements = []
for p in path:
if isinstance(p, int):
elements.append(f"[{p}]")
else:
elements.append(str(p))
return elements
def __eq__(self, other):
return isinstance(other, JsonSchemaValidator) and self.schema == other.schema