Django-rest-framework: Spécification de différents sérialiseurs pour l'entrée et la sortie

Créé le 2 mai 2014  ·  23Commentaires  ·  Source: encode/django-rest-framework

Je trouve que, en particulier en ce qui concerne les méthodes POST, j'ai souvent besoin d'un sérialiseur différent pour l'entrée que pour la sortie. Par exemple, pour un modèle particulier, je n'aurai peut-être besoin que de deux ou trois valeurs d'entrée, mais le serveur calculera/récupérera/quelque soit certaines valeurs supplémentaires pour les champs du modèle, et toutes ces valeurs doivent être renvoyées au client.

Jusqu'à présent, ma méthode a consisté à remplacer get_serializer_class() pour spécifier un sérialiseur d'entrée distinct pour la demande, puis à remplacer create() pour utiliser un sérialiseur différent pour ma sortie. Ce modèle fonctionne, mais il m'a fallu un certain temps pour le comprendre car la documentation ne suggère pas vraiment cette option ; l'hypothèse sur laquelle les APIViews génériques sont construites est que vous spécifiez un sérialiseur et qui est utilisé à la fois pour l'entrée et la sortie. Je suis d'accord que cela fonctionne généralement dans le cas courant, mais l'utilisation de ma méthode brise un peu la magie CBV. En particulier, il peut être difficile à résoudre si vous faites une erreur en spécifiant un sérialiseur de sortie personnalisé.

Je propose deux solutions :

  1. Créer un moyen cohérent de spécifier éventuellement différents sérialiseurs d'entrée et de sortie, ou
  2. Ajoutez de la documentation supplémentaire expliquant la présomption qu'un sérialiseur est destiné à l'entrée _et_ à la sortie, et les meilleures pratiques pour remplacer cela dans le cas où le comportement par défaut ne convient pas à votre cas d'utilisation.
Documentation

Commentaire le plus utile

Ouais, je vais bien de toute façon, honnêtement. J'ai juste eu du mal à comprendre comment faire ce que je voulais à partir des documents, mais mon incompréhension de la façon dont DRF est conçu pour fonctionner est peut-être aussi importante.

Voici un exemple pour que vous puissiez voir de quoi je parle; n'hésitez pas à noter si je fais quelque chose de monumentalement stupide et qu'il y a/devrait y avoir un meilleur moyen dans DRF lui-même qui me manque.

from rest_framework import generics, status
from rest_framework.response import Response

from rack.models import RackItem
from rack.serializers import RackItemSerializer, NewRackItemSerializer


class ListCreateRackItem(generics.ListCreateAPIView):
    model = RackItem

    def get_serializer_class(self):
        if self.request.method == 'POST':
            return NewRackItemSerializer
        return RackItemSerializer

    def get_queryset(self):
        return RackItem.objects.filter(shopper=self.request.user)

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.DATA)

        if not serializer.is_valid():
            return Response(
                serializer.errors, status=status.HTTP_400_BAD_REQUEST)

        item = RackItem.objects.create(
            shopper=request.user,
            item_url=serializer.data['item_url'],
            item_image_url=serializer.data['item_image_url'])

        result = RackItemSerializer(item)
        return Response(result.data, status=status.HTTP_201_CREATED)


class GetUpdateDeleteRackItem(generics.RetrieveUpdateDestroyAPIView):
    model = RackItem
    serializer_class = RackItemSerializer

    def get_queryset(self):
        return RackItem.objects.filter(shopper=self.request.user)

et les sérialiseurs eux-mêmes :

from rest_framework import serializers

from models import RackItem


class RackItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = RackItem


class NewRackItemSerializer(serializers.Serializer):
    item_url = serializers.URLField()
    item_image_url = serializers.URLField()

Tous les 23 commentaires

Vous devez utiliser les kwargs read/write_only si vous souhaitez renvoyer des données différentes mais autoriser l'écriture dans d'autres attributs.
Si vous avez besoin d'un formatage entièrement différent ou même de classes de champ différentes pour le même attribut, alors oui, vous devez utiliser deux sérialiseurs différents.

Salut @foresmac ,

Vous avez touché l'un des... euh... _points d'apprentissage_ du DRF. Ce genre de chose revient plusieurs fois sur StackOverflow.

Le remplacement de get_serializer_class() fonctionne assez bien, donc je privilégierais votre option 2.

Si vous avez envie de rédiger des modifications dans une pull request – ou de faire un article de blog ou quelque chose comme ça – ce serait cool.

En attendant, je vais clore ce sujet particulier.

@carltongibson :

Le remplacement de get_serializer_class() ne fonctionne que si vous utilisez un sérialiseur différent pour différentes méthodes HTTP, n'est-ce pas ? Existe-t-il un moyen de le remplacer afin qu'il renvoie un sérialiseur différent pour l'entrée sur un request rapport à la sortie sur un Response ?

@foresmac - je vois - un cas légèrement différent. Je pense que la réponse courte est "Pas automatiquement, pas actuellement". J'imagine que la chose la plus simple (si cela est vraiment nécessaire) est de définir les données de réponse (avec votre sérialiseur _output_ à la main. (Mais vous avez trouvé votre propre chemin via create , semble-t-il.)

si c'est vraiment nécessaire

Vous pouvez vraiment aller loin avec les champs en lecture seule et ainsi de suite - je peux certainement croire qu'il y a des cas où cela ne suffit pas mais je ne suis pas du tout sûr que de tels cas tomberaient dans le 80:20 qui doit être servi (dans le noyau) par DRF.

Si vous pensez qu'il nous manque quelque chose, je vous recommande de l'expliquer en profondeur, de montrer où le code changerait, de montrer quels cas d'utilisation seraient résolus par celui-ci - si cela sonne bien, ouvrez une demande d'extraction à cet effet afin que il peut être revu.

Si vous avez envie de faire un saut à l'option 2, ce sera toujours bien reçu.

Ouais, je vais bien de toute façon, honnêtement. J'ai juste eu du mal à comprendre comment faire ce que je voulais à partir des documents, mais mon incompréhension de la façon dont DRF est conçu pour fonctionner est peut-être aussi importante.

Voici un exemple pour que vous puissiez voir de quoi je parle; n'hésitez pas à noter si je fais quelque chose de monumentalement stupide et qu'il y a/devrait y avoir un meilleur moyen dans DRF lui-même qui me manque.

from rest_framework import generics, status
from rest_framework.response import Response

from rack.models import RackItem
from rack.serializers import RackItemSerializer, NewRackItemSerializer


class ListCreateRackItem(generics.ListCreateAPIView):
    model = RackItem

    def get_serializer_class(self):
        if self.request.method == 'POST':
            return NewRackItemSerializer
        return RackItemSerializer

    def get_queryset(self):
        return RackItem.objects.filter(shopper=self.request.user)

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.DATA)

        if not serializer.is_valid():
            return Response(
                serializer.errors, status=status.HTTP_400_BAD_REQUEST)

        item = RackItem.objects.create(
            shopper=request.user,
            item_url=serializer.data['item_url'],
            item_image_url=serializer.data['item_image_url'])

        result = RackItemSerializer(item)
        return Response(result.data, status=status.HTTP_201_CREATED)


class GetUpdateDeleteRackItem(generics.RetrieveUpdateDestroyAPIView):
    model = RackItem
    serializer_class = RackItemSerializer

    def get_queryset(self):
        return RackItem.objects.filter(shopper=self.request.user)

et les sérialiseurs eux-mêmes :

from rest_framework import serializers

from models import RackItem


class RackItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = RackItem


class NewRackItemSerializer(serializers.Serializer):
    item_url = serializers.URLField()
    item_image_url = serializers.URLField()

L'essentiel ici est que je ne reçois que quelques informations pour créer un élément de rack; le serveur lui-même génère tous les autres champs du modèle et les stocke dans la base de données. Mais, je veux que mon point de terminaison crache toutes ces informations lorsqu'un nouvel élément est créé.

Le modèle n'est pas trop difficile, mais tous les documents semblent faire beaucoup d'hypothèses selon lesquelles tout le monde fait le cas courant à 80%, et ce qu'il y a là-bas rend difficile de voir quoi remplacer ou où atteindre d'autres fins. Si faire ce que je fais n'est pas assez courant pour être traité dans le code, je suis plus qu'heureux de fournir un exemple de code et une explication de son fonctionnement et pourquoi.

@foresmac — il me semble que vous

_Cependant_, je suppose que vous pourriez obtenir le même résultat en marquant les champs fournis par le serveur - shopper dans votre exemple - comme read-only dans RackItemSerializer , puis utilisez-le. — Je suppose que c'est juste une question de ce que vous préférez au final.

Je pense que nous devrions préciser que nous préférons les champs en lecture seule afin de sécher le code.
La création de deux sérialiseurs semble redondante.
@foresmac Qu'en pensez-vous ?

Il y aura plus de champs modifiables plus tard, principalement des champs booléens qui enregistrent certaines actions de l'utilisateur avec le modèle, donc je ne suis pas sûr que cela résolve le problème à long terme. D'accord que je devrais probablement mieux utiliser read_only , cependant. Peut-être que la définition explicite de required dans certains cas peut également aider ? Je ne suis pas sûr, TBH.

J'ai l'habitude de créer un formulaire Django à utiliser pour la validation des entrées, et de créer simplement un dict de key_name, des paires de valeurs et de faire simplement json.dumps() pour la sortie. L'ensemble du concept d'un sérialiseur qui fonctionne dans les deux sens m'était complètement étranger avant d'utiliser DRF.

La réponse que je suis venu ici pour trouver s'est avérée être:

Si read_only ne fournit pas le contrôle dont vous avez besoin et que vous souhaitez personnaliser la logique de validation d'entrée ou de sortie, vous devez remplacer respectivement to_internal_value() ou to_representation() .

Dans ce cas, vous utiliseriez to_internal_value() pour personnaliser la génération de validated_data partir de ce que le client fournit. Si l'appel intégré suivant à YourModel.objects.create(**validated_data) ne fonctionne pas, vous pouvez alors remplacer create() .

J'essaie de résoudre ce problème en ce moment.
Je vais essayer ce package - https://github.com/vintasoftware/drf-rw-serializers.

Votre approche semble logique, pourquoi ne pas simplement en faire un mixin et le réutiliser. Quelque chose du genre :

from rest_framework import status
from rest_framework.response import Response


class DifferentOutInViewsetSerializers:
    """
    Mixin for allowing the use of different serializers for responses and
    requests for update/create
    """
    request_serializer_update = None
    response_serializer_update = None

    request_serializer_create = None
    response_serializer_create = None

    def get_serializer_class(self):
        if self.action == 'update':
            return self.request_serializer_update
        elif self.action == 'create':
            return self.request_serializer_create
        return super().get_serializer_class()

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)

        response_serializer = self.response_serializer_create(
            instance=serializer.instance)

        headers = self.get_success_headers(response_serializer.data)
        return Response(
            response_serializer.data,
            status=status.HTTP_201_CREATED, headers=headers)

    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        if getattr(instance, '_prefetched_objects_cache', None):
            # If 'prefetch_related' has been applied to a queryset, we need to
            # forcibly invalidate the prefetch cache on the instance.
            instance._prefetched_objects_cache = {}

        response_serializer = self.response_serializer_update(
            instance=serializer.instance)
        return Response(response_serializer.data)

J'en ai aussi besoin, mais dans mon cas, ce n'est pas pour les requêtes POST, mais une requête GET qui renvoie un tas d'objets non modaux, et j'ai besoin d'un tas de paramètres de filtre qui ne sont pas directement liés aux champs sur les objets.

Je ne sais pas si je fais quelque chose de bizarre ou si je manque complètement quelque chose, car je ne peux pas être le premier à avoir un point de terminaison qui n'est pas un modèle CRUD et qui doit générer la documentation à partir du point de terminaison ?

Je peux simplement récupérer les arguments de l'objet de requête, mais j'essaie de faire en sorte que mon API s'auto-documente en utilisant le générateur de documentation API dans drf, ou le package drf-yasg, d'où ma raison de vouloir utiliser les sérialiseurs pour en précisant les paramètres.
Existe-t-il une autre façon de décrire les paramètres de point de terminaison en plus des sérialiseurs ?

Désolé si cela n'entre pas dans le cadre de ce problème.

Auparavant, j'ai dit que j'allais utiliser https://github.com/vintasoftware/drf-rw-serializers.
Après 6 mois d'utilisation, cela m'a vraiment aidé. Pour la plupart des problèmes mentionnés ici, cette bibliothèque vous aidera.

@Moulde Jetez un œil à la lib que get_serializer_class - http://www.django-rest-framework.org/api-guide/generic-views/#get_serializer_classself.

@frenetic Cela semble résoudre un problème que `get_serializer_class' peut déjà résoudre la plupart du temps. Ce que @moulde (qui présente un cas d'utilisation légèrement différent de celui que j'ai mentionné précédemment) dit, c'est qu'il y a des moments où vous voulez / devez utiliser différents sérialiseurs pour la demande par rapport à la réponse. Et pouvoir tirer parti de la documentation automatique est une considération importante pour cela dans mon esprit, en dehors du désir d'éviter le code passe-partout dans ce cas.

Oui, fondamentalement, je pense que DRF a un support loin d'être idéal pour les vues non modales, où vous souhaitez utiliser un sérialiseur pour décrire l'interface, afin que la documentation automatique puisse être générée.
Un exemple pourrait être un point de terminaison pour le filtrage/la recherche de données non modales (externes).

il y a des moments où vous voulez/devez utiliser différents sérialiseurs pour la demande par rapport à la réponse

C'était une grande raison d'utiliser https://github.com/limdauto/drf_openapi (quand il était encore maintenu). Ce serait génial s'il était possible de différencier les schémas de requête et de réponse dans django-rest-framework. Il arrive souvent que nous étendions une autre API qui présente ces caractéristiques.

Je pense que la prise en charge de APIView pour différents sérialiseurs pour l'entrée et la sortie serait une fonctionnalité qui tue. Cela pourrait même être facile à mettre en œuvre ; par exemple, un paramètre pourrait être fourni à get_serializer afin que get_serializer_class connaisse le contexte (entrée vs sortie). Cela semble être assez discret et permettrait aux applications ayant besoin de cette fonctionnalité de suivre les conseils habituels de « remplacer get_serializer_class ».

En l'absence de cela, je voulais mettre en évidence quelques fonctionnalités actuelles de django-rest-framework je n'ai pas vues dans ce fil et qui pourraient aider quelqu'un d'autre à venir ici plus tard. S'il vous est possible d'inclure tous vos champs d'entrée et de sortie souhaités dans un seul Model , vous pouvez utiliser un ModelSerializer pour spécifier des champs en lecture seule et en écriture seule de manière à obtenir une entrée différente. et les schémas de sortie.
(Versions : djangorestframework 3.10.3, Django 2.2.5, Python 3.7.4)

Donc un Model comme ceci :

from django.db import models

class ThingModel(models.Model):
    input_field = models.CharField()
    output_date = models.DateTimeField()
    output_id = models.IntegerField()
    output_string = models.CharField()

Et un Serializer comme celui-ci :

from rest_framework import serializers
from .models import ThingModel

class ThingSerializer(serializers.ModelSerializer):
    class Meta:
        model = ThingModel
        fields = [
            "input_field",
            "output_date",
            "output_id",
            "output_string",
        ]
        read_only_fields = [
            "output_date",
            "output_id",
            "output_string",
        ]
        extra_kwargs = {"input_field": {"write_only": True}}

Produira les documents OpenAPI suivants lorsque generateschema est utilisé :

...
  /urlpath/:
    post:
      operationId: CreateThing
        parameters: []
        requestBody:
          content:
            application/json:
              schema:
                properties:
                  input_field:
                    type: string
                    write_only: true
                required:
                - input_field
        responses:
          '200':
            content:
              application/json:
                schema:
                  properties:
                    output_date:
                      type: string
                      format: date-time
                      readOnly: true
                    output_id:
                      type: integer
                      readOnly: true
                    output_string:
                      type: string
                      readOnly: true
                  required: []
          description: ''

Cela aide lorsque vous voulez qu'un point de terminaison, dans le contexte d'une seule méthode (par exemple POST ), ait des schémas d'entrée et de sortie différents, si l'objet entier peut être exprimé sous la forme d'un Model . Ce serait génial d'avoir des fonctionnalités similaires sans avoir à utiliser un ModelSerializer .

Quelle est la solution « de pointe » pour ce problème ? Y a-t-il eu une progression depuis 2014 ?

@fronbasal Nous avons accompli cela avec des Serializer s en utilisant write_only=True et read_only=True sur chaque sérialiseur Field comme il convient. Je ne sais pas à quoi ressemblent les documents Swagger générés pour cela, mais ils accomplissent des schémas différents pour l'entrée et la sortie dans une mesure suffisante pour nos besoins.

Je ne sais pas si les responsables ont une solution différente/meilleure en tête

@matthewwithanm D'accord, merci beaucoup !

@fronbasal j'ai trouvé une solution simple, il suffit de passer le modèle à un autre sérialiseur

voici l'exemple

def create(self, request):
    serializer = InputSerializer(data=request.data)
    output_serializer = serializer # default
    if not serializer.is_valid():
        return Response(
             {'errors': serializer.errors},
            status=status.HTTP_400_BAD_REQUEST)
    try:
        output_serializer = OutputSerializer(serializer.save())
    except Exception as e:
        raise ValidationError({"detail": e})
    return Response(output_serializer.data, status=status.HTTP_201_CREATED)

À votre santé

@michaelhenry c'est une solution vraiment astucieuse ! Aimer.

Cette page vous a été utile?
0 / 5 - 0 notes