Валидация полей Json в моделях Django

Spread the love

Иногда приходятся сталкиваться с задачей хранения JSON данных в моделях Django. В этом нам очень хорошо помогает сама Django, так как она поддерживает тип данных БД на основе JSON. JSONField() может быть назначен атрибутам models для хранения данных на основе JSON. Это очень простой путь решения подобных задач, но у него есть одна большая побочная проблема. При большом использование полей JSONField в проекте ломается консистентность данных, и возникает проблема контролирования структуры всех данных в БД. Если на это не обращать внимание через некоторое время в БД может быть записано все что угодно и это значительно усложнит работу с проектом.

К счастью, есть отличный инструмент для проверки данных JSON. Он называется json-schema и имеет реализации на нескольких языках, включая Python. Он может проверять типы данных, убедиться, что строки соответствуют перечисленным атрибутам и разрешать/запрещать дополнительные свойства, которые могут не требовать какой-либо проверки. Вы также можете описывать разрешенные типы данных, например, как простые int, string, так и составные массивы, объекты, где каждый элемент имеет заданное количество полей с разными типами данных.

Создадим пример структуры данных используемой для хранения SEO данных веб страницы на двух языках:

    {
        "en": {
            "image": {
                "src": None,
            },
            "title": None,
            "description": None,
            "keywords": None,
        },
        "ru": {
            "image": {
                "src": None,
            },
            "title": None,
            "description": None,
            "keywords": None,
        },
    }

Вот пример схемы json-schema, по которой мы можем проверить наши данные.

schema': 'http://json-schema.org/draft-07/schema#',
    'type': 'object',
    'properties': {
        'ru': {
            'type': ["object"],
            'properties': {
                'image': {
                    'type': ["object"],
                    'properties': {
                        'src': {
                            'type': ["string", "null"],
                        },
                    },
                    'required': ['src', ],
                    'additionalProperties': False,
                },
                'title': {
                    'type': ["string", "null"]
                },
                'keywords': {
                    'type': ["string", "null"]
                },
                'description': {
                    'type': ["string", "null"]
                },
            },
            'required': ['image', 'title', 'keywords', 'description'],
            'additionalProperties': False,
        },
        'en': {
            'type': ["object"],
            'properties': {
                'image': {
                    'type': ["object"],
                    'properties': {
                        'src': {
                            'type': ["string", "null"],
                        },
                        
                    },
                    'required': ['src', ],
                    'additionalProperties': False,
                },
                'title': {
                    'type': ["string", "null"]
                },
                'keywords': {
                    'type': ["string", "null"]
                },
                'description': {
                    'type': ["string", "null"]
                },
            },
            'required': ['image', 'title', 'keywords', 'description'],
            'additionalProperties': False,
        },
    },
    'required': ['ru', 'en'],
    'additionalProperties': False,

Из имен атрибутов можно догадаться о их предназначение. За более подробным описание обратитесь к официальной документации, там все просто.

Далее нам нужно связать нашу схему с полем в котором будут храниться данные. Мы рассмотрим два варианта.

Вариант с расширением поля

В этом варианте, мы расширим JSONField (), предоставляемый Django, и добавим в него функцию проверки перед сохранением.

from jsonschema import validate, exceptions as jsonschema_exceptions

from django.core import exceptions
from django.contrib.postgres.fields import JSONField


class JSONSchemaField(JSONField):

    def __init__(self, *args, **kwargs):
        self.schema = kwargs.pop('schema', None)
        super().__init__(*args, **kwargs)

    @property
    def _schema_data(self):
        model_file = inspect.getfile(self.model)
        dirname = os.path.dirname(model_file)
        # schema file related to model.py path
        p = os.path.join(dirname, self.schema)
        with open(p, 'r') as file:
            return json.loads(file.read())

    def _validate_schema(self, value):

        # Disable validation when migrations are faked
        if self.model.__module__ == '__fake__':
            return True
        try:
            status = validate(value, self._schema_data)
        except jsonschema_exceptions.ValidationError as e:
            raise exceptions.ValidationError(e.message, code='invalid')
        return status

    def validate(self, value, model_instance):
        super().validate(value, model_instance)
        self._validate_schema(value)

    def pre_save(self, model_instance, add):
        value = super().pre_save(model_instance, add)
        if value and not self.null:
            self._validate_schema(value)
        return value

Здесь мы используем реализацию jsonschema, которая проверяет наши данные по схеме, такой как определенная выше. В конструкторе мы ожидаем путь к файлу json, который затем загружаем в память как свойство в методе _schema_data. Метод pre_save обеспечивает выполнение проверки перед сохранением экземпляра модели.

Использование этого поля довольно просто. Нам нужно будет определить схему и сохранить ее, прежде чем указывать относительный путь к ней в качестве аргумента:

class Page(models.Model):
    title = models.CharField(max_length=256)
    content = models.TextField()
    seo = JSONSchemaField(
      schema='schemas/jsonschema.example.json', default=dict, blank=True)

Теперь, если мы попытаемся сохранить json, который не соответствует схеме, определенной выше, будет сгенерирована ошибка ValidationError.

Вариант с использованием Django валидаторов

Для него создадим класс валидатора

import django
from django.core.validators import BaseValidator

import jsonschema

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

Далее используем его при определение поля

from common.validators import (
    JSONSchemaValidator,
    SEO_JSON_FIELD_SCHEMA,
)
class Page(models.Model):
    title = models.CharField(max_length=256)
    content = models.TextField()
    seo = JSONSchemaField(
      default=dict, blank=True,
      validators=[JSONSchemaValidator(limit_value=SEO_JSON_FIELD_SCHEMA)],
    )

Этот вариант содержит значительно меньше кода, но у него и меньше гибкости.

Заключение

Использование jsonschema отличный способ автоматизировать проверку json вместо того, чтобы делать это в ручную в сериализаторах или где-либо еще. Мы можем пойти дальше и создать настраиваемое свойство схемы, которое может отличается для каждого экземпляра модели. Для этого нам нужно будет передать схему в поле при сохранении экземпляра и вручную вызвать наш метод проверки.

Была ли вам полезна эта статья?
[0 / 0]

Spread the love
Подписаться
Уведомление о
guest
1 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Станислав
Станислав
2 лет назад

Мне для json больше нравится pydantic.