Валидация полей Json в моделях Django
Иногда приходятся сталкиваться с задачей хранения 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 вместо того, чтобы делать это в ручную в сериализаторах или где-либо еще. Мы можем пойти дальше и создать настраиваемое свойство схемы, которое может отличается для каждого экземпляра модели. Для этого нам нужно будет передать схему в поле при сохранении экземпляра и вручную вызвать наш метод проверки.
Мне для json больше нравится pydantic.