django postgresql json field schema validation

后端 未结 4 1332
半阙折子戏
半阙折子戏 2021-02-05 09:59

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?

相关标签:
4条回答
  • 2021-02-05 10:04

    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)
    
    0 讨论(0)
  • 2021-02-05 10:18

    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.')
    
    0 讨论(0)
  • 2021-02-05 10:19

    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)

    0 讨论(0)
  • 2021-02-05 10:22

    I wrote a custom validator using jsonschema in order to do this (Django 1.11, Python 3.6).

    project/validators.py

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

    project/app/models.py

    from django.db import models
    from django.contrib.postgres.fields import JSONField
    
    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 = JSONField(
            default=dict,
            validators=[JSONSchemaValidator(limit_value=MY_JSON_FIELD_SCHEMA)]
        )
    
    0 讨论(0)
提交回复
热议问题