Sauvegarder de la donnée dans une table d’une base de donné relationnelle en utilisant du JSON apporte les avantages suivants:
- De la flexibilité: il est possible de modifier la structure de la donnée sans avoir besoin de modifier le schema de la base de donnée. A titre d’exemple, il est commun d’utiliser un
JSONFieldpour stocker des métadonnées en provenance de services tiers. - Possibilité de sauvegarder des modèles de données complexes dans la base de donnée avec un faible impact sur les opérations de lecture et d’écriture, évitant des requêtes complexes impliquant des jointures couteuses.
Depuis la version 3.1 de Django, le champs de modèle JSONField est disponible pour tous les moteurs de base de données supportés par le le framework. Précédemment, ce champs faisait parti du package django.contrib.postgres.fields et était limité aux base de données PostgreSQL.
Un des inconvénients auquel vous pourriez faire face en utilisant des JSONField dans votre projet Django est que la donnée contenue dans ce type de champs risque de devenir imprévisible:
- vous pouvez difficilement être certains de ce qui va être stocké dans ces champs, au fil du temps, dans votre base de données de production.
- en tant que développeur, il peut être difficile de savoir précisément comment est structuré le contenu d’un
JSONFieldet de savoir ce qu’il contient (sans avoir à relire beaucoup de code).
Dans cet article, nous allons essayer de répondre à cette problématique en:
- Utilisant des JSON Schema afin de valider le contenu d’un
dict. - Créant notre propre champs de model Django,
JSONSchemaField, afin d’être en mesure de déclarer unJSON Schemaet de l’utiliser pour valider le contenu d’un json field avant son insertion dans la base de donnée.
Les sources de ce projet sont disponibles sur Github.

A propos de JSON Schema
JSONSchema est un vocabulaire que vous pouvez utiliser pour annoter et valider des documents JSON: https://json-schema.org/learn/getting-started-step-by-step#creating-your-first-schema.
En bref, un JSONSchema vous permet de facilement déclarer:
- La liste des propriétés de votre objet
JSON. - pour chaque propriété, vous pouvez valider son type (et, optionnellement, son format).
- chaque propriété peut être, au choix, requise ou optionnelle.
- il est possible d’autoriser, ou non, l’ajout de propriété non listées dans le
JSON Schema.
Voici un exemple assez simple qui représente un produit avec un nom et un prix:
{
"title": "Product",
"description": "A product from Acme's catalog",
"type": "object",
"properties": {
"productId": {
"description": "The unique identifier for a product",
"type": "integer"
},
"productName": {
"description": "Name of the product",
"type": "string"
},
"price": {
"description": "The price of the product",
"type": "number",
"exclusiveMinimum": 0
},
"tags": {
"description": "Tags for the product",
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"uniqueItems": true
}
},
"required": [ "productId", "productName", "price" ]
}
Dans cet exemple:
titleetdescriptionsont des propriété purement informatives. Ces mots clé n’ajoutent pas de contrainte au schéma de données, vous n’avez pas à les déclarer (mais vous pouvez les utiliser en tant que documentation naturelle dans votre code).typeest un mot clé de validation. La donnée saisie doit correspondre au type déclaré dans le schéma.- Afin d’être considéré comme valide, notre objet
JSONdoit contenir les 3 propriétés suivantes:productId,productName,price. La propriététagsest optionnelle car non déclarée dans le champsrequiredde notre schéma. - Vous pouvez interdire l’ajout, à un objet
JSON, de propriétés non listées dans le schéma en utilisant le mot clé additionalProperties et en définissant sa valeur àfalse.
Vous pouvez trouver plus d’exemples sur la page miscellaneous-examples de la documentation.
python-jsonschema
Nous allons utiliser la librairie python-jsonschema afin de valider le contenu d’un dictionnaire Python, en se basant sur un JSON Schema. Cette libraire est une implémentation des spécification de JSON Schema pour Python.
pip install jsonschema
Voici un exemple simple tiré de la documentation:
>>> from jsonschema import validate
>>> # A sample schema, like what we'd get from json.load()
>>> schema = {
"type" : "object",
"properties" : {
"price" : {"type" : "number"},
"name" : {"type" : "string"},
},
}
>>> # If no exception is raised by validate(), the instance is valid.
>>> validate(instance={"name" : "Eggs", "price" : 34.99}, schema=schema)
>>> validate(
>>> instance={"name" : "Eggs", "price" : "Invalid"}, schema=schema,
>>> )
Traceback (most recent call last):
...
ValidationError: 'Invalid' is not of type 'number'
Ajout de la validation à vos JSONFields Django
Comme nous avons pu le voir juste au-dessus, la libraire python-jsonschema est assez simple d’utilisation. Nous voulons maintenant l’utiliser pour valider le contenu de nos JSONField Django avant de le sauvegarder dans notre base de donnée. Afin d’y arriver, nous allons créer notre propre champs de model Django que nous allons appeler JSONSchemaField. Ce champs va hériter du champs JSONField disponible avec Django, nous n’allons pas modifier son comportement initiale mais nous allons lui ajouter une couche de validation.
Nous allons prendre l’exemple d’un model Django destiné à sauvegarder des informations complémentaires au sujet d’un utilisateur. Notre but est d’arriver au model suivant:
from django.db import models
from django.contrib.auth.models import User
from django.conf import settings
from .fields import JSONSchemaField
class UserInformation(models.Model)
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
information = JSONSchemaField(
schema={
"type": "object",
"properties": {
"name": {"type": "string", "maxLength": 255},
"email": {"type": "string", "format": "email"},
"centers_of_interest": {
"type": "array",
"items": {
"type": "string",
"maxLength": 255,
},
},
},
"required": ["name", "email"],
"additionalProperties": False,
},
)
Ce model déclare:
- Une relation 1:1 vers un utilisateur.
- Un champs
informationutilisé pour sauvegarder les informations complémentaires au formatJSON.
Bien sur, il ne s’agit que d’un exemple simple pour valider notre démonstration. Vous avez sûrement des cas d’utilisation plus intéressants!
Nous allons commencer par créer un nouveau fichier (que je vais nommer fields.py) et le placer dans une app Django:
from django.db import models
from django.core import checks, exceptions
from jsonschema import SchemaError
from jsonschema import exceptions as jsonschema_exceptions
from jsonschema import validate
from jsonschema.validators import validator_for
class JSONSchemaField(models.JSONField):
"""
JSONField with a schema validation by using `python-jsonschema`.
Cf: https://python-jsonschema.readthedocs.io/en/stable/validate/
"""
def __init__(self, *args, **kwargs):
self.schema = kwargs.pop("schema", None)
super().__init__(*args, **kwargs)
def validate(self, value, model_instance):
return super().validate(value, model_instance)
def check(self, **kwargs):
return super().check(**kwargs)
Notre nouveau champs déclare:
- un argument nommé
schema. Il sera utilisé pour déclarer le schéma json à appliquer pour valider la donnée au niveau du champs de notre model. - Une fonction
validate(): elle est responsable de valider le contenu qui s’apprête à être stocké dans le champs. Cette fonction est utilisé par Django afin de valider des formulaires, sur l’admin, dans le serializers DRF … Si vous n’êtes pas familier avec cette fonction, je vous recommande la lecture de la page Form and field validation documentation. - Une fonction
check(): elle est responsable d’exécuter des vérifications, de valider que notre champs est correctement implémenté au niveau de notre model (qu’il est au bon type et qu’il représente un schéma json valide). Les Django’s system checks sont exécutés au lancement du serveur Django (ou bien lorsque vous lancezpython manage.py check) et vous informent d’erreurs présentes dans votre projet. Pour cette implémentation, je me suis inspiré de ce que fait Django pour le champs de model CharField.
Les checks
Je recommande de débuter par l’implémentation des checks. Cette manière de faire devrait vous faire gagner du temps en vous assurant que vous travailler sur un schéma JSON valide.
La méthode check() est prototypée de la manière suivante:
- Elle prends des
**kwargsqui contiennent les paramètres que vous déclarez au niveau de votre champs (par exemple,max_lengthpour unCharField). Nous allons recevoir le paramètreschemaparmi leskwargs. - Elle retourne une liste d’objets
CheckMessage(une liste vide signifiant qu’il n’y à pas d’erreurs) pouvant chacun avoir différents niveaux de sévérité (Debug,Info,Warning,Error,Critical). Si la méthodecheck()retourne un ou des messages avec un niveau de sévérité égal ou supérieur àError, alors Django empêchera toute commande de management de s’exécuter. Autrement, les messages seront reportés dans la console mais n’empêcheront pas le serveur de démarrer.
Nous implémentons notre check de la manière suivante:
from jsonschema.validators import validator_for
class JSONSchemaField(models.JSONField):
def __init__(self, *args, **kwargs):
self.schema = kwargs.pop("schema", None)
super().__init__(*args, **kwargs)
@property
def _has_valid_schema(self) -> bool:
if not isinstance(self.schema, dict):
return False
# Determine validator class for the schema.
schema_cls = validator_for(self.schema)
try:
# Check the schema.
schema_cls.check_schema(self.schema)
except SchemaError:
return False
return True
def _check_schema_attribute(self):
if self.schema is None:
return [
checks.Error(
"JSONSchemaField must define a 'schema' attribute.", obj=self
)
]
elif not self._has_valid_schema:
return [
checks.Error("Given 'schema' is not a valid json schema.", obj=self)
]
else:
return []
def check(self, **kwargs):
return [*super().check(**kwargs), *self._check_schema_attribute()]
Nous effectuons les vérifications suivantes sur le paramètre schema:
- S’assurer qu’il s’agit d’une instance de
dict. - S’assurer qu’il s’agit d’un schéma JSON valide et conforme aux spécifications.
- Pendant cette étape, nous appelons la fonction validator_for(). Cette fonction est capable de déterminer la classe de validation appropriée pour valider un schéma JSON. Par exemple, si vous déclarer la propriété $schema dans votre schéma, elle sera utilisée pour déterminer la classe à utiliser pour la validation de ce dernier.
Vous pouvez valider que votre implémentation est correcte en exécutant le code suivant:
>>> python manage.py check
System check identified no issues (0 silenced).
Maintenant, essayez de remplacer le paramètre schema de votre champs de model en le modifiant de la façon suivante:
information = JSONSchemaField(
schema={
"type": "foo",
"properties": {
},
},
)
>>> python manage.py check
SystemCheckError: System check identified some issues:
ERRORS:
jsonschemafield.UserInformation.information: Given 'schema' is not a valid json schema.
System check identified 1 issue (0 silenced).
La validation
Il est maintenant temps de valider le contenu s’apprêtant à être stocké en base de données via notre JSON Field.
Pour ce faire, nous modifions la méthode JSONSchemaField.validate() afin qu’elle appelle la fonction validate() de la librairie python-jsonschema.
from jsonschema import validate
class JSONSchemaField(models.JSONField):
def validate(self, value, model_instance):
"""Validate the content of the json field."""
super().validate(value, model_instance)
try:
validate(instance=value, schema=self.schema)
except jsonschema_exceptions.ValidationError as e:
raise exceptions.ValidationError(
"Invalid json content: %(value)s",
code="invalid_content",
params={"value": e.message},
)
Vous pouvez valider que votre implémentation fonctionne en jouant avec l’admin Django ou bien en ouvrant un shell:
>>> from myapp.models import UserInformation
>>> from django.contrib.auth import get_user_model
>>> User = get_user_model()
>>> user = User.objects.get(pk=42)
>>> user_information = UserInformation(user=user, information={'name': 'Joe', 'email': 'joe@fasterthan.fr'})
>>> user_information.full_clean()
>>> user_information = UserInformation(user=user, information={'name': 'Joe', 'email': 'joe@fasterthan.fr', 'foo': 'bar'})
>>> user_information.full_clean()
---------------------------------------------------------------------------
ValidationError Traceback (most recent call last)
Cell In[9], line 1
----> 1 user_information.full_clean()
File ~/.venvs/blog-entry-jsonschemafield/lib/python3.10/site-packages/django/db/models/base.py:1552, in Model.full_clean(self, exclude, validate_unique, validate_constraints)
1549 errors = e.update_error_dict(errors)
1551 if errors:
-> 1552 raise ValidationError(errors)
ValidationError: {'information': ["Invalid json content: Additional properties are not allowed ('foo' was unexpected)"]}
(Optionnel) Valider le format d’une propriété
JSONSchema expose un mot clé format qui peut être utilisé pour valider le contenu d’une propriété. Par exemple, il est possible de valider que la propriété représente une adresse mail correctement formatée.
Regardons à nouveau le schéma que nous avons déclaré plus tôt:
information = JSONSchemaField(
schema={
"type": "object",
"properties": {
"name": {"type": "string", "maxLength": 255},
"email": {"type": "string", "format": "email"},
"centers_of_interest": {
"type": "array",
"items": {
"type": "string",
"maxLength": 255,
},
},
},
"required": ["name", "email"],
"additionalProperties": False,
},
)
Vous pouvez voir que la propriété email déclare un champs format de type email. Seulement, si nous tentons de valider un contenu comportant une adresse mail mal formée:
>>> user_information = UserInformation(user=user, information={'name': 'Joe', 'email': 'bar'})
>>> user_information.full_clean()
aucune ValidationError n’est levée cette fois ci …
C’est parce que la spécification de jsonschema ne force pas la validation des formats. La librairie python suit donc ce comportement: https://python-jsonschema.readthedocs.io/en/stable/validate/#validating-formats. Il est toutefois possible de valider le format de nos propriétés en mettant à jour notre code afin de passer format_checker en argument de la fonction validate() de python-jsonschema:
class JSONSchemaField(models.JSONField):
def validate(self, value, model_instance):
super().validate(value, model_instance)
# Determine validator class for the schema ...
schema_cls = validator_for(self.schema)
try:
validate(
instance=value,
schema=self.schema,
# ... use it to validate the content of the json.
format_checker=schema_cls.FORMAT_CHECKER,
)
except jsonschema_exceptions.ValidationError as e:
raise exceptions.ValidationError(
"Invalid json content: %(value)s",
code="invalid_content",
params={"value": e.message},
)
Vous pouvez maintenant relancer le même code encore une fois:
>>> user_information = UserInformation(user=user, information={'name': 'Joe', 'email': 'bar'})
>>> user_information.full_clean()
---------------------------------------------------------------------------
ValidationError Traceback (most recent call last)
Cell In[7], line 1
----> 1 user_information.full_clean()
File ~/.venvs/blog-entry-jsonschemafield/lib/python3.10/site-packages/django/db/models/base.py:1552, in Model.full_clean(self, exclude, validate_unique, validate_constraints)
1549 errors = e.update_error_dict(errors)
1551 if errors:
-> 1552 raise ValidationError(errors)
ValidationError: {'information': ["Invalid json content: 'bar' is not a 'email'"]}
Veuillez noter que, tel que spécifié dans la documentation de python-jsonschema:
- certains formats nécessite l’installation de dépendances python supplémentaires.
- ce n’est pas le cas du format
emailque nous utilisons dans cet exemple (et d’autres formats tels quedateou bienipv4). - si ces dépendances requises ne sont pas installées, alors la validation va réussir sans lever d’erreurs liées aux formats.
Si vous souhaitez installer ces dépendances supplémentaires, vous pouvez le faire de la manière suivante:
pip install jsonschema'[format]'
ou bien
pip install jsonschema'[format-nongpl]'
La seconde manière de faire n’installera pas de dépendances python couverte par une licence GPL. Veuillez noter que si votre application utilise des librairies couvertes par la licence GPL alors cela implique pour votre logiciel d’être distribué sous la licence GPL.
Pour conclure
Nous arrivons au terme de cet article. J’espère qu’il vous aura intéressé et que vous y aurez appris des choses.
Voici la version finale du code que nous avons produit:
fields.py
from django.db import models
from django.core import checks, exceptions
from jsonschema import SchemaError
from jsonschema import exceptions as jsonschema_exceptions
from jsonschema import validate
from jsonschema.validators import validator_for
class JSONSchemaField(models.JSONField):
"""
JSONField with a schema validation by using `python-jsonschema`.
Cf: https://python-jsonschema.readthedocs.io/en/stable/validate/
"""
def __init__(self, *args, **kwargs):
self.schema = kwargs.pop("schema", None)
super().__init__(*args, **kwargs)
@property
def _has_valid_schema(self):
"""Check that the given `schema` is a valid json schema."""
schema_cls = validator_for(self.schema)
try:
schema_cls.check_schema(self.schema)
except SchemaError:
return False
return True
def check(self, **kwargs):
return [*super().check(**kwargs), *self._check_schema_attribute()]
def _check_schema_attribute(self):
"""Ensure that the given schema is a valid json schema during Django's checks."""
if self.schema is None:
return [
checks.Error(
"JSONSchemaField must define a 'schema' attribute.", obj=self
)
]
elif not self._has_valid_schema:
return [
checks.Error("Given 'schema' is not a valid json schema.", obj=self)
]
else:
return []
def validate(self, value, model_instance):
"""Validate the content of the json field."""
super().validate(value, model_instance)
schema_cls = validator_for(self.schema)
try:
validate(instance=value, schema=self.schema, format_checker=schema_cls.FORMAT_CHECKER)
except jsonschema_exceptions.ValidationError as e:
raise exceptions.ValidationError(
"Invalid json content: %(value)s",
code="invalid_content",
params={"value": e.message},
)
models.py
from django.db import models
from django.contrib.auth.models import User
from django.conf import settings
from .fields import JSONSchemaField
class UserInformation(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
information = JSONSchemaField(
schema={
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {"type": "string", "maxLength": 255},
"email": {"type": "string", "format": "email"},
"centers_of_interest": {
"type": "array",
"items": {
"type": "string",
"maxLength": 255,
},
},
},
"required": ["name", "email"],
"additionalProperties": False,
},
)
Les sources de ce projet sont disponibles sur github.
Le code que nous avons produit va valider le contenu de vos JSONField dans les formulaires, sur l’admin Django, les serializers DRF, … En fonction de votre projet, vous pourriez vouloir forcer la validation à la sauvegarde d’un model. C’est possible en appelant le fonction full_clean() dans la méthode save() de votre model:
class UserInformation(models.Model):
def save(self, *args, **kwargs):
super().full_clean()
super().save(*args, **kwargs)
Veuillez noter que la validation des JSONSchemaField se fait au niveau applicatif (via du code Python) et non au niveau de votre base de données. Cela signifie que cela ne fonctionnera pas sur des création ou mise à jour en batch (eg: bluk_create).
Il est aussi possible de réaliser des validations de propriété d’un objet JSON de manière plus poussée que ce que nous avons fait aujourd’hui. Par exemple, le schéma suivant peut être utiliser pour valider qu’une url commence par https:
"url": {"type": "string", "format": "uri", "pattern": "^https://"},