ev350
ev350

Reputation: 439

Django Rest Framework POST and GET Nested Serializers

I've been developing my own API for a Kanban-Style Project Board. I have attached a UML diagram to show how the "boards" application is organised.

My Application's Model UML Diagram

My problem is that when I want to create a new Card I want to be able to create the card with a list of Labels by Primary Keys passed in the POST parameters, like so:

{
    "title": "Test Card",
    "description": "This is a Test Card!",
    "created_by": 1,
    "labels": [1,2]
}

Another requirement I have is that I would like to retrieve the serialized labels as part of the card object, like so:

{
    "id": 1,
    "board": 1,
    "title": "Some Card",
    "description": "The description of Some Card.",
    "created_by": 1,
    "assignees": [
        {
            "id": 1,
            "username": "test1",
            "email": "[email protected]"
        }
    ],
    "labels": [
        {
            "id": 1,
            "board": 1,
            "title": "Pink Label",
            "color": "#f442cb"
        }
    ],
    "comment_set": []
}

I am going to assume that to achieve this difference in POST and GET functionality I am going to have to have 2 different serializers?

However, the main question of this post has to do with the creation logic from the POST data as mentioned above. I keep getting errors like this:

{
    "labels": [
        {
            "non_field_errors": [
                "Invalid data. Expected a dictionary, but got int."
            ]
        },
        {
            "non_field_errors": [
                "Invalid data. Expected a dictionary, but got int."
            ]
        }
    ]
}

I have tried many different combinations of DRF Serializers in my CardSerializer but always end up with error messages that have the same format as above: "Expected but got ". Any help or pointers, even someone telling me that this is bad REST design for example, would be greatly appreciated! :)

EDIT: I should add that in the case I change the CardSerializer labels field from a LabelSerializer to a PrimaryKeyRelatedField (as the comment in the code shows) I receive the following error:

Direct assignment to the forward side of a many-to-many set is prohibited. Use labels.set() instead.

Here are the relevant parts of my source code:

models.py

class Card(models.Model):
"""Represents a card."""

    # Parent
    board = models.ForeignKey(Board, on_delete=models.CASCADE)
    column = models.ForeignKey(Column, on_delete=models.CASCADE, null=True)

    # Fields
    title = models.CharField(max_length=255, null=False)
    description = models.TextField()

    assignees = models.ManyToManyField(User, blank=True, related_name='card_assignees')
    labels = models.ManyToManyField(Label, blank=True, related_name='card_labels')

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(blank=True, null=True)  # Blank for django-admin

    created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='card_created_by')

views.py

class CardList(generics.ListCreateAPIView):
    queryset = Card.objects.all()
    serializer_class = CardSerializer

    def get_queryset(self):
        columns = Column.objects.filter(board_id=self.kwargs['board_pk'])
        queryset = Card.objects.filter(column__in=columns)
        return queryset

    def post(self, request, *args, **kwargs):
        board = Board.objects.get(pk=kwargs['board_pk'])

        post_data = {
            'title': request.data.get('title'),
            'description': request.data.get('description'),
            'created_by': request.data.get('created_by'),
            'assignees': request.data.get('assignees'),
            'labels': request.data.get('labels'),
        }
        serializer = CardSerializer(data=post_data, context={'board': board})

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status.HTTP_201_CREATED)
        return Response(serializer.errors, status.HTTP_400_BAD_REQUEST)

serializers.py

class UserSerializer(serializers.ModelSerializer):
    """Serializer to map the User instance to JSON."""

    class Meta:
        model = User
        fields = ('id', 'username', 'email')


class CommentSerializer(serializers.ModelSerializer):
    """Serializer to map the Comment instance to JSON."""

    class Meta:
        model = Comment
        fields = '__all__'


class LabelSerializer(serializers.ModelSerializer):
    """Serializer to map the Label instance to JSON."""

    class Meta:
        model = Label
        fields = ('id', 'board', 'title', 'color')


class CardSerializer(serializers.ModelSerializer):
    """Serializer to map the Card instance to JSON."""
    assignees = UserSerializer(many=True, read_only=True)
    labels = LabelSerializer(many=True)
    comment_set = CommentSerializer(many=True, read_only=True)

    # assignees = PrimaryKeyRelatedField(many=True, read_only=True)
    # labels = PrimaryKeyRelatedField(many=True, queryset=Label.objects.all())

    def create(self, validated_data):
        board = self.context['board']
        card = Card.objects.create(
            board=board,
            **validated_data
        )
        return card

    class Meta:
        model = Card
        fields = ('id', 'board', 'title', 'description', 'created_by', 'assignees', 'labels', 'comment_set')
        read_only_fields = ('id', 'board')

Upvotes: 5

Views: 3483

Answers (2)

Tek Kshetri
Tek Kshetri

Reputation: 2337

If anyone stuck on the same problem, then here is the solution. I think you need to create the two serializer classes, one for the get request and another for the post request. And call the required serializer from viewset as below,

class MyModelViewSet(viewsets.MyModelViewSet):

    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer # default serializer, you can change this to MyModelListSerializer as well

    action_serializers = {
        'list': MyModelListSerializer, # get request serializer
        'create': MyModelCreateSerializer # post request serializer
    }

    def get_serializer_class(self):

        if hasattr(self, 'action_serializers'):
            return self.action_serializers.get(self.action, self.serializer_class)

        return super(MyModelViewSet, self).get_serializer_class()

here is the example of the MyModelListSerializer and MyModelCreateSerializer,

# Used for the get request
class MyModelListSerializer(serializers.ModelSerializer):
    assignees = AssigneesSerializer(read_only=True, many=True)
    labels = LabelsSerializer(read_only=True, many=True)

    class Meta:
        model = MyModel
        fields = '__all__'

# Used for the post request
class MyModelCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = MyModel
        fields = "__all__"

Upvotes: 2

ev350
ev350

Reputation: 439

I managed to find a solution, however this may not conform to best practice. If anyone could clarify this that would be great. However, for now:

I changed the create function in the CardSerializer to the following:

def create(self, validated_data):
    board = self.context['board']
    labels_data = validated_data.pop('labels')
    card = Card.objects.create(
        board=board,
        **validated_data
    )
    card.labels.set(labels_data)
    return card

The card.labels.set(labels_data) line means I bypass the following error message:

Direct assignment to the forward side of a many-to-many set is prohibited.

Which is why I am uncertain if it is the "correct" thing to do but it seems to work for now.

Upvotes: 3

Related Questions