Makina Blog

Le blog Makina-corpus

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

Nantes Du 12 au 14 mars 2024

Voir la formation

Formations Django

Formation Django Rest Framework

À distance (FOAD) Du 10 au 14 juin 2024

Voir la formation

Formations Django

Formation Django avancé

À distance (FOAD) Du 9 au 13 décembre 2024

Voir la formation

Actualités en lien

Image
Django_Rest_Framework_routeurs2
06/01/2016

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.

Voir l'article
Image
Django_Rest_Framework_routeurs3
06/01/2016

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.

Voir l'article
15/12/2015

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.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus