33

I have a django model with a JSONField (django.contrib.postgres.fields.JSONField) Is there any way that I can validate model data against a json schema file?

(pre-save)
Something like my_field = JSONField(schema_file=my_schema_file)

1

6 Answers 6

42

I wrote a custom validator using jsonschema in order to do this.

project/validators.py

import django
from django.core.validators import BaseValidator
import jsonschema
    

class JSONSchemaValidator(BaseValidator):
    def compare(self, value, schema):
        try:
            jsonschema.validate(value, schema)
        except jsonschema.exceptions.ValidationError:
            raise django.core.exceptions.ValidationError(
                '%(value)s failed JSON schema check', params={'value': value}
            )

project/app/models.py

from django.db import models

from project.validators import JSONSchemaValidator

MY_JSON_FIELD_SCHEMA = {
    'schema': 'http://json-schema.org/draft-07/schema#',
    'type': 'object',
    'properties': {
        'my_key': {
            'type': 'string'
        }
    },
    'required': ['my_key']
}

class MyModel(models.Model):
    my_json_field = models.JSONField(
        default=dict,
        validators=[JSONSchemaValidator(limit_value=MY_JSON_FIELD_SCHEMA)]
    )
Sign up to request clarification or add additional context in comments.

2 Comments

For those who this did't work completely, i had to add this function Model.clean_fields() and then Model.save() depending on the change
If you have performance issues validating deeply nested JSON structures, see fastjsonschema. The speed up for us was incredible.
8

That's what the Model.clean() method is for (see docs). Example:

class MyData(models.Model):
    some_json = JSONField()
    ...

    def clean(self):
        if not is_my_schema(self.some_json):
            raise ValidationError('Invalid schema.')

1 Comment

But clean method is not call automatically. Right ?
8

you could use cerberus to validate your data against a schema

from cerberus import Validator

schema = {'name': {'type': 'string'}}
v = Validator(schema)
data = {'name': 'john doe'}
v.validate(data)  # returns "True" (if passed)
v.errors  # this would return the error dict (or on empty dict in case of no errors)

it's pretty straightforward to use (also due to it's good documentation -> validation rules: http://docs.python-cerberus.org/en/stable/validation-rules.html)

Comments

2

I wrote a custom JSONField that extends models.JSONField and validates attribute's value by using jsonschema (Django 3.1, Python 3.7).

I didn't use the validators parameter for one reason: I want to let users define the schema dynamically.So I use a schema parameter, that should be:

  1. None (by default): the field will behave like its parent class (no JSON schema validation support).
  2. A dict object. This option is suitable for a small schema definition (for example: {"type": "string"});
  3. A str object that describes a path to the file where the schema code is contained. This option is suitable for a big schema definition (to preserve the beauty of the model class definition code). For searching I use all enabled finders: django.contrib.staticfiles.finders.find().
  4. A function that takes a model instance as an argument and returns a schema as dict object. So you can build a schema based on the state of the given model instance. The function will be called every time when the validate() is called.

myapp/models/fields.py

import json

from jsonschema import validators as json_validators
from jsonschema import exceptions as json_exceptions

from django.contrib.staticfiles import finders
from django.core import checks, exceptions
from django.db import models
from django.utils.functional import cached_property


class SchemaMode:
    STATIC = 'static'
    DYNAMIC = 'dynamic'


class JSONField(models.JSONField):
    """
    A models.JSONField subclass that supports the JSON schema validation.
    """
    def __init__(self, *args, schema=None, **kwargs):
        if schema is not None:
            if not(isinstance(schema, (bool, dict, str)) or callable(schema)):
                raise ValueError('The "schema" parameter must be bool, dict, str, or callable object.')
            self.validate = self._validate
        else:
            self.__dict__['schema_mode'] = False
        self.schema = schema
        super().__init__(*args, **kwargs)

    def check(self, **kwargs):
        errors = super().check(**kwargs)
        if self.schema_mode == SchemaMode.STATIC:
            errors.extend(self._check_static_schema(**kwargs))
        return errors

    def _check_static_schema(self, **kwargs):
        try:
            schema = self.get_schema()
        except (TypeError, OSError):
            return [
                checks.Error(
                    f"The file '{self.schema}' cannot be found.",
                    hint="Make sure that 'STATICFILES_DIRS' and 'STATICFILES_FINDERS' settings "
                         "are configured correctly.",
                    obj=self,
                    id='myapp.E001',
                )
            ]
        except json.JSONDecodeError:
            return [
                checks.Error(
                    f"The file '{self.schema}' contains an invalid JSON data.",
                    obj=self,
                    id='myapp.E002'
                )
            ]

        validator_cls = json_validators.validator_for(schema)

        try:
            validator_cls.check_schema(schema)
        except json_exceptions.SchemaError:
            return [
                checks.Error(
                    f"{schema} must be a valid JSON Schema.",
                    obj=self,
                    id='myapp.E003'
                )
            ]
        else:
            return []

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        if self.schema is not None:
            kwargs['schema'] = self.schema
        return name, path, args, kwargs

    @cached_property
    def schema_mode(self):
        if callable(self.schema):
            return SchemaMode.DYNAMIC
        return SchemaMode.STATIC

    @cached_property
    def _get_schema(self):
        if callable(self.schema):
            return self.schema
        elif isinstance(self.schema, str):
            with open(finders.find(self.schema)) as fp:
                schema = json.load(fp)
        else:
            schema = self.schema
        return lambda obj: schema

    def get_schema(self, obj=None):
        """
        Return schema data for this field.
        """
        return self._get_schema(obj)

    def _validate(self, value, model_instance):
        super(models.JSONField, self).validate(value, model_instance)
        schema = self.get_schema(model_instance)
        try:
            json_validators.validate(value, schema)
        except json_exceptions.ValidationError as e:
            raise exceptions.ValidationError(e.message, code='invalid')

Usage: myapp/models/__init__.py

def schema(instance):
    schema = {}
    # Here is your code that uses the other
    # instance's fields to create a schema.
    return schema


class JSONSchemaModel(models.Model):
    dynamic = JSONField(schema=schema, default=dict)
    from_dict = JSONField(schema={'type': 'object'}, default=dict)

    # A static file: myapp/static/myapp/schema.json
    from_file = JSONField(schema='myapp/schema.json', default=dict)

Comments

2

Another solution using jsonschema for simple cases.

class JSONValidatedField(models.JSONField):
    def __init__(self, *args, **kwargs):
        self.props = kwargs.pop('props')
        self.required_props = kwargs.pop('required_props', [])
        super().__init__(*args, **kwargs)

    def validate(self, value, model_instance):
        try:
            jsonschema.validate(
                value, {
                    'schema': 'http://json-schema.org/draft-07/schema#',
                    'type': 'object',
                    'properties': self.props,
                    'required': self.required_props
                }
            )
        except jsonschema.exceptions.ValidationError:
            raise ValidationError(
                    f'Value "{value}" failed schema validation.')


class SomeModel(models.Model):
    my_json_field = JSONValidatedField(
            props={
                'foo': {'type': 'string'}, 
                'bar': {'type': 'integer'}
            }, 
            required_props=['foo'])

Comments

0
from typing import Literal
from django.db import models
from django.core.exceptions import ValidationError as DjValidationError

from pydantic import BaseModel
from pydantic_core import ValidationError


class ColumnModel(BaseModel):
    type: Literal["Decimal", "str"]
    name: str


class TableSchemaModel(BaseModel):
    cols: list[ColumnModel]


def validate_schema(val):
    try:
        TableSchemaModel(**val)
    except ValidationError as err:
        raise DjValidationError(str(err))


class Table(models.Model):
    name = models.CharField(max_length=200)
    schema = models.JSONField(
        default=dict,
        blank=True,
        validators=(validate_schema,),
        verbose_name="Схема",
    )

    def __str__(self):
        return self.name

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.