26

Consider a Listing model that has an associated Category. I want to create a new Listing for an existing Category by doing a POST with data: {"title": "myapp", "category": {"name": "Business"}}, where title is the title of the Listing that should be created, and Business is the name of an existing category to use for this new listing.

When I try to make such a request and instantiate the ListingSerializer for this, I get an error indicating that the Category name must be unique - I don't want to create a new Category, but use an existing one instead. I've tried setting the validators on the category field to [], but that didn't change the behavior.

I can use a SlugRelatedField, but that forces my request data to look more like {"title": "myapp", "category": "Business"}, which isn't what I want. I tried using the source argument for the SlugRelatedField to specify a nested relationship, but that didn't work either:

category = serializers.SlugRelatedField(
        slug_field='category.name',
        queryset=models.Category.objects.all()
    )

yields:

  "category": [
    "Object with name={'name': 'Business'} does not exist."
  ]

models.py:

import django.contrib.auth
from django.db import models
from django.conf import settings

class Profile(models.Model):
    display_name = models.CharField(max_length=255)
    user = models.OneToOneField(settings.AUTH_USER_MODEL)

class Category(models.Model):
    name = models.CharField(max_length=50, unique=True)
    description = models.CharField(max_length=200)

class Listing(models.Model):
    title = models.CharField(max_length=50, unique=True)
    category = models.ForeignKey(Category, related_name='listings', null=True)
    owners = models.ManyToManyField(
        Profile,
        related_name='owned_listings',
        db_table='profile_listing',
        blank=True
    )

serializers.py:

import logging
import django.contrib.auth
from rest_framework import serializers
import myapp.models as models

logger = logging.getLogger('mylogger')

class ShortUserSerializer(serializers.ModelSerializer):
    class Meta:
        model = django.contrib.auth.models.User
        fields = ('username', 'email')

class ProfileSerializer(serializers.ModelSerializer):
    user = ShortUserSerializer()
    class Meta:
        model = models.Profile
        fields = ('user', 'display_name')
        read_only = ('display_name',)

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Category
        fields = ('name', 'description')
        read_only = ('description',)


class ListingSerializer(serializers.ModelSerializer):
    owners = ProfileSerializer(required=False, many=True)
    # TODO: how to indicate that this should look for an existing category?
    category = CategorySerializer(required=False, validators=[])

    class Meta:
        model = models.Listing
        depth = 2

    def validate(self, data):
        logger.info('inside ListingSerializer validate')
        return data

    def create(self, validated_data):
        logger.info('inside ListingSerializer.create')
        # not even getting this far...

views.py:

import logging

from django.http import HttpResponse
from django.shortcuts import get_object_or_404

import django.contrib.auth

from rest_framework import viewsets
from rest_framework.response import Response

import myapp.serializers as serializers
import myapp.models as models


# Get an instance of a logger
logger = logging.getLogger('mylogger')

class CategoryViewSet(viewsets.ModelViewSet):
    queryset = models.Category.objects.all()
    serializer_class = serializers.CategorySerializer

class UserViewSet(viewsets.ModelViewSet):
    queryset = django.contrib.auth.models.User.objects.all()
    serializer_class = serializers.ShortUserSerializer

class ProfileViewSet(viewsets.ModelViewSet):
    queryset = models.Profile.objects.all()
    serializer_class = serializers.ProfileSerializer

class ListingViewSet(viewsets.ModelViewSet):
    logger.info('inside ListingSerializerViewSet')
    queryset = models.Listing.objects.all()
    serializer_class = serializers.ListingSerializer

Full example: https://github.com/arw180/drf-example

arw180
  • 391
  • 1
  • 3
  • 10

3 Answers3

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
  • 391
  • 1
  • 3
  • 10
  • 1
    You should accept this answer as it remains the "state of the art" (c.f. the [github issue](https://github.com/encode/django-rest-framework/issues/2996)). That ticket also has a link to a post by a core contributor that documents roughly the same. – claytond Nov 07 '17 at 16:08
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())

    ...
Community
  • 1
  • 1
demux
  • 4,544
  • 2
  • 32
  • 56
  • 1
    The validation errors come at the time the serializer is instantiated in the `ModelViewSet`, so even with these changes I still have the error. – arw180 Aug 21 '15 at 11:00
  • Ok, I'll look into it. – demux Aug 21 '15 at 11:32
  • Although my use of `partial=True` is more concise, your solution will likely be better once I add support for the PUT request as well. It sill uses the `extra_kwargs` to remove the validators for the `CategorySerializer` class, which isn't ideal, though Tom Christie himself opted not to use the `ModelSerializer` in the answer you linked to, one benefit being it would avoid this problem with the auto-generated field validators. I'll probably accept this unless someone figures out a cleaner method using `SlugRelatedField` – arw180 Aug 21 '15 at 15:05
  • If you try things such as leaving the field empty or undefined, you'll see that there is still validation being done. – demux Aug 21 '15 at 15:48
  • Oh, you don't want this to create a new category if one doesn't exist with the requested name? – demux Aug 23 '15 at 02:15
  • Correct - in my case, it is invalid to create a new Listing for a Category that does not already exist – arw180 Aug 24 '15 at 10:40
  • regarding your `SlugRelatedField` example - my issue is when trying to use the slug field to reference a nested resource. For example, by setting the `slug_field` to `category.name` – arw180 Aug 25 '15 at 11:18
  • What you're effectively getting by that is `category.category.name`. – demux Aug 25 '15 at 17:10
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
  • 1,529
  • 1
  • 15
  • 8