Accueil / Blog / Métier / 2015 / Django Rest Framework : les Serializer et les exceptions (partie 1)

Django Rest Framework : les Serializer et les exceptions (partie 1)

Par Yannick Chabbert — publié 16/12/2015
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 : les Serializer et les exceptions (partie 1)

Django Rest Framework

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éan, 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 :
    {"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 :
    {"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 dictionaire 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.

ABONNEZ-VOUS À LA NEWSLETTER !
Voir aussi
Python : Bien configurer son environnement de développement Python : Bien configurer son environnement de développement 07/12/2015

Comment utiliser les bonnes pratiques de développement Python.

Formation Django initiation à Toulouse du 13 au 15 mars Formation Django initiation à Toulouse du 13 au 15 mars 26/01/2017

Entrez de plain-pied dans l'univers de Django aux côtés de développeurs ayant une expérience de ...

Alone in the cloud Alone in the cloud 14/12/2016

Thoughts while helping my brother-in-law

Retour sur la PyConFr 2016 Retour sur la PyConFr 2016 18/10/2016

Nous étions présents à Rennes pour PyConFr 2016. Voici notre compte-rendu à chaud.

Wagtail: How to use the Page model and its manager (part 2) Wagtail: How to use the Page model and its manager (part 2) 08/08/2016

The Page model has several methods specific to Wagtail. This is also the case of its manager. We ...