Search code examples
djangodjango-modelsdjango-rest-frameworkdjango-serializer

Many to Many field and Nested Serializer in REST: Overwriting Nested serializer doesn't create nested object in Django


I have two models with a many to many relation and I am trying to send nested data to my API. Unfortunately it only gives me back an empty array.

This is what I am trying:

my models:


class Building(models.Model):
    name  = models.CharField(max_length=120, null=True, blank=True)
    net_leased_area = models.FloatField(null=True, blank=True)

class BuildingGroup(models.Model):
    description           = models.CharField(max_length=500, null=True, blank=True)
    buildings             = models.ManyToManyField(Building, default=None, blank=True)

My generic API view:

class BuildingGroupCreateAPIView(CreateAPIView):
    queryset                    = BuildingGroup.objects.all()
    serializer_class            = BuildingGroupSerializer

My serializer:


class BuildingGroupSerializer(serializers.ModelSerializer):

    buildings = BuildingSerializer(many=True)

    class Meta:

        model = BuildingGroup

        fields = (
            'description',
            'buildings',
        )

    def create(self, validated_data):
        buildings_data = validated_data.pop('buildings')
        building_group = BuildingGroup.objects.create(**validated_data)
        for building_data in buildings_data:
            Building.objects.create(building_group=building_group, **building_data)
        return building_group

When I send data it returns this:


{"description":"Test Description API","buildings":[]}

In the array I would like to have my array of dictionaries.

I am trying to follow the REST documentation here when I am overwriting the create method to send a nested object. (https://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers) and I thought I am doing this correctly, but epic fail.

I send data with request with my custom method like this:

test_api_local(method="post", data={
        "description": "Test Description API",
        "buildings": [{'name' : 'Testname'}, .... ],
         })

Any help is very appreciated. Thanks so much!!

EDIT: When I try to test it on the REST view it tells me:

TypeError: 'building_group' is an invalid keyword argument for this function

EDIT2: Here is my view:

class BuildingGroupCreateAPIView(CreateAPIView):
    queryset                    = BuildingGroup.objects.all()
    serializer_class            = BuildingGroupSerializer

    def create(self, request, *args, **kwargs):
        serializer = BuildingGroupSerializer(data=self.request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data)

Solution

  • You have to explicitly get or create the Building instances, depending upon passed id's inside the payload data, then add them to BuildingGroup instance.

    class NestedBuildingSerializer(serializers.ModelSerializer):
        id = serializers.IntegerField(required=False)
    
        class Meta:
            model = Building
            fields = '__all__'
    
    
    class BuildingGroupSerializer(serializers.ModelSerializer):
        buildings = NestedBuildingSerializer(many=True)
    
        class Meta:
            model = BuildingGroup
            fields = (
                'description',
                'buildings',
            )
    
        def create(self, validated_data):
            buildings_data = validated_data.pop('buildings')
            building_group = BuildingGroup.objects.create(**validated_data)
            buildings = []  # it will contains list of Building model instance
            for building_data in buildings_data:
                building_id = building_data.pop('id', None)
                building, _ = Building.objects.get_or_create(id=building_id,
                                                             defaults=building_data)
                buildings.append(building)
            # add all passed instances of Building model to BuildingGroup instance
            building_group.buildings.add(*buildings)
            return building_group
    
    class BuildingGroupView(ListAPIView, CreateAPIView):
        queryset = BuildingGroup.objects.all()
        serializer_class = BuildingGroupSerializer
    
    
    ## Assume you add your views like this in urls.py
    urlpatterns = [
        .....
        path('building-groups', views.BuildingGroupView.as_view(),
             name='building-group'),
        .....
    ]
    

    On calling endpoint /building-groups as POST method with payload like this:

    {
      "description": "here description",
      "buildings": [
        {
          "id": 1, # existing building of id 1
          "name": "name of building 1",
          "net_leased_area": 1800
        },
        { 
          # a new building will gets create
          "name": "name of building 2",
          "net_leased_area": 1800
        }
      ]
    }
    

    Then , it will return response like this:-

    {
      "description": "here description",
      "buildings": [
        {
          "id": 1,
          "name": "name of building 1",
          "net_leased_area": 1800
        },
        {
          "id": 2
          "name": "name of building 2",
          "net_leased_area": 1800
        }
      ]
    }
    

    Learn about M2M relationship and, .get_or_create()

    Note: BuildingSerializer and NestedBuildingSerializer are both different. Don't mix them up.