[Django]-Writable nested serializer with existing objects using Django Rest Framework 3.2.2

5👍

This isn’t ideal, but I did find a solution that solved my problem (I’m waiting to accept it as the answer, hoping someone else can do better). There are two parts:

First, use the partial=True argument when initializing the ListingSerializer ( http://www.django-rest-framework.org/api-guide/serializers/#partial-updates). Then use the serializer’s validate method to get the actual model instance corresponding to the input data.

Second, explicitly remove the validators for the name field in the CategorySerializer. This is especially lousy because it effects more than just the ListingSerializer.

Leaving out either piece will result in the validation errors being thrown at the time the serializer is instantiated.

modifications to views.py:

class ListingViewSet(viewsets.ModelViewSet):
    queryset = models.Listing.objects.all()
    serializer_class = serializers.ListingSerializer

    def create(self, request):
        serializer = serializers.ListingSerializer(data=request.data,
            context={'request': request}, partial=True)
        if not serializer.is_valid():
            logger.error('%s' % serializer.errors)
            return Response(serializer.errors,
                  status=status.HTTP_400_BAD_REQUEST)

        serializer.save()
        return Response(serializer.data, status=status.HTTP_201_CREATED)

modifications to serializers.py:

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Category
        fields = ('name', 'description')
        read_only = ('description',)
        # also need to explicitly remove validators for `name` field
        extra_kwargs = {
            'name': {
                'validators': []
            }
        }

class ListingSerializer(serializers.ModelSerializer):
    owners = ProfileSerializer(required=False, many=True)
    category = CategorySerializer(required=False)
    class Meta:
        model = models.Listing
        depth = 2

    def validate(self, data):
        # manually get the Category instance from the input data
        data['category'] = models.Category.objects.get(name=data['category']['name'])
        return data

    def create(self, validated_data):
        title = validated_data['title']

        listing = models.Listing(title=validated_data['title'],
            category=validated_data['category'])

        listing.save()

        if 'owners' in validated_data:
            logger.debug('owners: %s' % validated_data['owners'])
            for owner in validated_data['owners']:
                print ('adding owner: %s' % owner)
                listing.owners.add(owner)
        return listing

I’ll wait a bit to accept this as the answer in case someone can come up with a better solution (like how to make the source argument work properly with a SlugRelatedField) – I have a working example using the solution above at https://github.com/arw180/drf-example if you want to experiment. I’d also love to hear comments regarding why the extra_kwargs stuff is necessary in the CategorySerializer – why isn’t instantiating it like this: category = CategorySerializer(required=False, validators=[]) sufficient (in the ListingSerializer)? UPDATE: I believe that doesn’t work because the unique validator is added automatically from the DB constraints and run regardless of any explicit validators set here, as explained in this answer: http://iswwwup.com/t/3bf20dfabe1f/python-order-of-serializer-validation-in-django-rest-framework.html

👤arw180

4👍

Turn CategorySerializer.create into an update_or_create method on name

class CategorySerializer(serializers.ModelSerializer):

    ...

    # update_or_create on `name`
    def create(self, validated_data):
        try:
            self.instance = Category.objects.get(name=validated_data['name'])
            self.instance = self.update(self.instance, validated_data)
            assert self.instance is not None, (
                '`update()` did not return an object instance.'
            )
            return self.instance
        except Category.DoesNotExist:
            return super(CategorySerializer, self).create(validated_data)

    ...

I recommend looking at the DRF source when ever you need to create custom functionality.

Related question answered by the creator of DRF: django-rest-framework 3.0 create or update in nested serializer

Edit

So I was still in the DRF 2 mindset where nested writable fields are handled automatically. You can read up on the subject here: http://www.django-rest-framework.org/topics/3.0-announcement/

I’ve tested the following code and it works:

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        ...
        extra_kwargs = {
            'name': {'validators': []},
            'description': {'required': False},
        }


class ListingSerializer(serializers.ModelSerializer):

    ...

    def update_or_create_category(self, validated_data):
        data = validated_data.pop('category', None)
        if not data:
            return None

        category, created = models.Category.objects.update_or_create(
            name=data.pop('name'), defaults=data)

        validated_data['category'] = category

    def create(self, validated_data):
        self.update_or_create_category(validated_data)
        return super(ListingSerializer, self).create(validated_data)

    def update(self, instance, validated_data):
        self.update_or_create_category(validated_data)
        return super(ListingSerializer, self).update(instance, validated_data)

Edit

The correct way of using SlugRelatedField is like this, in case you were wondering:

class ListingSerializer(serializers.ModelSerializer):

    ...

    # slug_field should be 'name', i.e. the name of the field on the related model
    category = serializers.SlugRelatedField(slug_field='name',
        queryset=models.Category.objects.all())

    ...
👤demux

1👍

I had similar problem: I needed to check if nested serializer (CategorySerializer) exists if yes to use it and if not – create it from nesting serializer (ListingSerializer). The solution of @demux totally worked for me only if I didn’t use custom validation for a field in nested serializer (the field by which I would check from nesting serializer if this instance exists). So I added create() method to nested serializer and @demux custom update_or_create_category(), create(), update() for ListingSerializer worked perfectly.

class CategorySerializer(serializers.ModelSerializer):

    class Meta:
    model = Category
    ...

    def create(self, validated_data):
        if Category.objects.filter(name=self.validated_data['name']).exists():
            raise serializers.ValidationError("This category name already exists")
        return Category.objects.create(**validated_data)
👤montxe

Leave a comment