Makina Blog
Django Rest Framework : les Serializer et les exceptions (partie 1)
Django Rest Framework est une extension à Django pour développer rapidement des API REST robustes au goût du jour. Reprenant la philosophie Django, la prise en main est rapide et efficace. Néanmoins, certaines notions nécessitent une attention plus particulière que nous allons mettre en avant dans cette suite d'articles.
Django Rest Framework (DRF) est, comme son nom l'indique, une extension à Django pour développer des API REST.
Dans cet article, nous supposons que vous êtes déjà à l'aise avec Django et les bonnes pratiques des API REST. De plus, vous avez déjà eu une première approche avec DRF. Vous connaissez donc les principes de bases de DRF.
Serializer
Les serializers peuvent être perçus comme des filtres d'entrées/sorties. Ils vous permettent donc de valider (entrées) ou formater (sorties) vos données. Autant leur usage est quasiment automatique avec des modèles, autant ils sont parfois oubliés pour des points d'entrée personnalisés quand on débute. Sauf cas exceptionnel (ce qui ne devrait pratiquement jamais arriver), les serializers répondront à tous vos besoins, vous feront gagner du temps, de la robustesse et de la lisibilité.
Car si vous n'utilisez pas les serializers :
- vous n'exploitez pas le potentiel de l'outil, les serializers existent précisément pour ça !
- vous devez vous-même refaire le parsing, la validation, la conversion, les erreurs HTTP, etc
- vous dupliquez le code pour chacune de vos vues : pas de mutualisation possible
- vous risquez probablement de déporter la logique métier dans vos vues
Entrées
En utilisant les serializers, vous vous déchargez de la lourde tâche de parsing, validation avec renvoi de code HTTP adéquat pour les erreurs rencontrées, etc.
Par exemple, ne manipulez pas request.data
ou request.query_params
directement (encore moins request.POST
ou request.GET
, voir la doc).
Concrètement, ne faites pas ça :
from django.db import models
from django.utils.translation import ugettext as _
from rest_framework import exceptions
from rest_framework.response import Response
from rest_framework.views import APIView
class MyObject(models.Model):
foo = models.CharField(max_length=128)
bar = models.CharField(max_length=128)
class MyView(APIView):
def get(self, request, *args, **kwargs):
pk = request.query_params.get('pk', None)
if pk is None:
raise exceptions.ValidationError({
'pk': [_('This field is required')]
})
try:
pk = int(pk)
except ValueError:
raise exceptions.ValidationError({
'pk': [_('This parameter must be an integer')]
})
if pk <= 0:
raise exceptions.ValidationError({
'pk': [_('Must be greater than zero.')]
})
myobj = MyObject.objects.get(pk=pk)
return Response({
'foo': myobj.foo,
'bar': myobj.bar,
})
Mais plutôt :
from django.db import models
from rest_framework import serializers
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from rest_framework.views import APIView
class MyObject(models.Model):
foo = models.CharField(max_length=128)
bar = models.CharField(max_length=128)
class MyInputSerializer(serializers.Serializer):
id = serializers.IntegerField(min_value=1)
class MyOutputSerializer(serializers.ModelSerializer):
class Meta:
model = MyObject
fields = ('foo', 'bar')
class MyView(APIView):
def get(self, request, *args, **kwargs):
input_serializer = MyInputSerializer(data=request.query_params)
input_serializer.is_valid(raise_exception=True)
instance = get_object_or_404(MyObject, pk=input_serializer.data['id'])
output_serializer = MyOutputSerializer(instance)
return Response(output_serializer.data)
Dans cet exemple, nous n'utilisons pas le même serializer pour l'entrée/sortie parce que les champs d'entrées sont radicalement différents des champs de sorties (id pour l'entrée, foo et bar pour la sortie). Bien sûr, nous pourrions utiliser la propriété Meta
du serializer read_only_fields
ou bien même les attributs de champs read_only
et write_only
. Mais conserver une séparation stricte est plus simple à maintenir.
Sortie
Pour formater vos données en sorties, les serializers s'avèrent également très efficaces. Il n'est plus nécessaire de penser à convertir les valeurs dans le bon format (décimal, booléen, etc).
Cela peut vous éviter un bon nombre d'erreurs/oublis et assurent ainsi la persistance de vos signatures de sorties (renvoyez null en JSON avec une clé qui existe toujours. Car une clé volatile dans la signature de sortie est un véritable cauchemar pour les clients qui intègrent votre API).
Documentation
Un autre avantage, et pas des moindres : les outils d'auto-documentation. Que ce soit la browsable API ou des outils externes comme Swagger (par le biais de Django Rest Swagger), ils s'appuient sur les serializers pour générer une bonne partie de la documentation d'API, comme par exemple les signatures d'entrées/sorties.
Enfin, l'API est incroyablement poussée et atteint aujourd'hui une maturité qui vous permet de facilement combiner/étendre les serializers afin de parfaitement coller à vos besoins (héritage, serializer relationnel, en liste, propriétés Meta, validateurs, tous types de champs, etc). Et ce, comme à l'accoutumée, pour le moins d'effort possible ;-)
Exceptions
DRF propose un système de gestion d'exceptions afin de renvoyer des réponses HTTP adéquates pour les erreurs rencontrées.
Autant les codes HTTP renvoyés sont triviaux :
- 400 = validation (bad request)
- 401 = authentification requise
- 403 = accès refusé
- 404 = ressource introuvable
- etc
Autant le format de la réponse (body) est varié et peut perturber les clients qui intègrent l'API.
En fait, la gestion des exceptions démarre dans la méthode dispatch
de la classe racine APIView
. En cas d'exception levée, un handler (configurable) sera appliqué. Par défaut, ce handler (rest_framework.views.exception_handler
) renvoie un dictionnaire avec la clé detail pour les exceptions dont le message (aka detail) n'est pas déjà une liste/dictionnaire et qui sont du type :
APIException
Http404
PermissionDenied
Sinon, le message associé à l'exception est inchangé et renvoyé dans la réponse.
Autrement dit, ce qu'il faut retenir pour vos clients d'API :
- le corps des réponses HTTP de type erreur (4xx) sont toujours des dictionnaires
- la structure de ce tableau est variable :
- le tableau contient une seule clé
detail
et contient le message d'erreur. Par exemple : .. code-block:: javascript {"detail": "Une erreur de type 401, 403, 404, etc."} - le tableau contient une ou plusieurs clés avec une liste de message d'erreurs associés (erreur 400). Une clé spéciale permet d'identifier les erreurs globales (par défaut
non_field_errors
). Par exemple : .. code-block:: javascript {"non_field_errors": ["Une erreur globale"], "a_field": ["Une erreur sur le champ 'a_field'"]}
Attention : on décrit ici les erreurs telles qu'elles sont gérées par DRF (via les serializers
, etc). Car évidemment, si vous souhaitez complètement changer ce formalisme, vous pouvez. DRF force uniquement la structure parente : renvoyer un(e) dictionnaire/liste. Libre à vous de faire comme bon vous semble. La principale règle à respecter pour vos clients étant la persistance/cohérence. Cependant pour du JSON et des raisons de sécurité, ne renvoyez que des dictionnaires. Jamais de liste.
Erreur de validation
Si vous n'utilisez pas un serializer (ça arrive !) et par conséquent devez gérer manuellement les exceptions de validation, voici un exemple d'usage :
from django.utils.translation import ugettext as _
from rest_framework import exceptions
from rest_framework.views import APIView
class MyView(APIView):
def post(self, request, *args, **kwargs):
username = request.data.get('username')
if username == 'foo':
raise exceptions.ValidationError({
'username': [ # Always use a list
_("A specific error for the field username. This is at "
"least how serializer do."),
],
})
raise exceptions.ValidationError({
self.settings.NON_FIELD_ERRORS_KEY: [ # Always use a list
_('A global error to throw.'),
]
})
Gardez à l'esprit de toujours passer un dictionnaire de listes pour l'argument detail
d'une ValidationError
. Ainsi le format d'erreur sera toujours le même. En effet, DRF ne vous interdit pas de fournir une chaîne plutôt qu'une liste. Concrètement, ceci est valide mais déconseillé :
raise ValidationError({'foo': 'An error on foo.'})
À venir
Dans cette première partie, nous avons parlé des serializers
et des exceptions
. Dans un prochain article, nous parlerons des Viewsets.
Formations associées
Formations Django
Formation Django initiation
À distance (FOAD) Du 4 au 8 novembre 2024
Voir la formationFormations Django
Formation Django REST Framework
À distance (FOAD) Du 9 au 13 juin 2025
Voir la formationFormations Django
Formation Django avancé
À distance (FOAD) Du 9 au 13 décembre 2024
Voir la formationActualités en lien
Django Rest Framework : intégration des routeurs (partie 4)
Comprendre comment les routeurs fonctionnent est une chose. Savoir s'en servir correctement, c'est mieux ! On va voir que ça n'est pas si simple et que c'est probablement une des parties de DRF la plus étonnante.
Django Rest Framework : fonctionnement des routeurs (partie 3)
Chose promise, chose due : après avoir mis en avant les bienfaits des Viewset, on va plonger dans le côté obscur de la force de DRF : les routeurs.
Django Rest Framework : les Viewset (partie 2)
Dans un précédent article, nous avions évoqué les bonnes pratiques à appliquer avec les serializers et les exceptions. Dans cet article, nous allons nous intéresser plus particulièrement aux Viewset.