Andrew
Andrew

Reputation: 135

Django rest framework nested serializer create method

I have created a nested serializer, when I try to post data in it it keeps on displaying either the foreign key value cannot be null or dictionary expected. I have gone through various similar questions and tried the responses but it is not working for me. Here are the models

##CLasses
class Classes(models.Model):
    class_name = models.CharField(max_length=255)
    class_code = models.CharField(max_length=255)
    created_date = models.DateTimeField(auto_now_add=True)
    def __str__(self):
        return self.class_name
    class Meta:
        ordering = ['class_code']
##Streams
class Stream(models.Model):
    stream_name = models.CharField(max_length=255)
    classes = models.ForeignKey(Classes,related_name="classes",on_delete=models.CASCADE)
    created_date = models.DateTimeField(auto_now_add=True)
    def __str__(self):
        return self.stream_name
    class Meta:
        ordering = ['stream_name']

Here is the view

class StreamViewset(viewsets.ModelViewSet):
    queryset = Stream.objects.all()
    serializer_class = StreamSerializer

Here is the serializer class

class StreamSerializer(serializers.ModelSerializer):
    # classesDetails = serializers.SerializerMethodField()
    classes = ClassSerializer()
    class Meta:
        model = Stream
        fields = '__all__'
    def create(self,validated_data):
        classes = Classes.objects.get(id=validated_data["classes"])
        return Stream.objects.create(**validated_data, classes=classes)
    # def perfom_create(self,serializer):
    #     serializer.save(classes=self.request.classes)
    #depth = 1
    # def get_classesDetails(self, obj):
    #     clas = Classes.objects.get(id=obj.classes)
    #     classesDetails =  ClassSerializer(clas).data
    #     return classesDetails

I have tried several ways of enabling the create method but like this displays an error {"classes":{"non_field_errors":["Invalid data. Expected a dictionary, but got int."]}}. Any contribution would be deeply appreciated

Upvotes: 6

Views: 4232

Answers (3)

Zekarias Taye Hirpo
Zekarias Taye Hirpo

Reputation: 849

You can also solve this issue in such a way,

Serializer class

# Classes serializer
class ClassesSerializer(ModelSerializer):
    class Meta:
        model = Classes
        fields = '__all__'

# Stream serializer
class StreamSerializer(ModelSerializer):
    classes = ClassesSerializer(read_only=True)
    class Meta:
        model = Stream
        fields = '__all__'

View

# Create Stream view
@api_view(['POST'])
def create_stream(request):
    classes_id = request.data['classes']  # or however you are sending the id
    serializer = StreamSerializer(data=request.data)

    if serializer.is_valid():
        classes_instance = get_object_or_404(Classes, id=classes_id)
        serializer.save(classes=classes_instance)
    else:
        return Response(serializer.errors)
    return Response(serializer.data)

Upvotes: 0

CarPobl
CarPobl

Reputation: 31

Kevin Languasco describes the behaviour of the create method quite well and his solutions are valid ones. I would add a variation to solution 1:

class StreamSerializer(serializers.ModelSerializer):
    classes = ClassSerializer(read_only=True)
    classes_id = serializers.IntegerField(write_only=True)

    def create(self,validated_data):
      return Stream.objects.create(**validated_data, classes=classes)

    class Meta:
      model = Stream
      fields = (
        'pk',
        'stream_name',
        'classes',
        'classes_id',
        'created_date',
    )

The serializer will work without overriding the create method, but you can still do so if you want to as in your example.

Pass the value classes_id in the body of your POST method, not classes. When deserializing the data, the validation will skip classes and will check classes_id instead.

When serializing the data (when you perform a GET request, for example), classes will be used with your nested dictionary and classes_id will be omitted.

Upvotes: 1

Kevin Languasco
Kevin Languasco

Reputation: 2427

This is a very common situation when developing APIs with DRF.

The problem

Before DRF reaches the create() method, it validates the input, which I assume has a form similar to

{
   "classes": 3,
   "stream_name": "example"
}

This means that, since it was specified that

classes = ClassSerializer()

DRF is trying to build the classes dictionary from the integer. Of course, this will fail, and you can see that from the error dictionary

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

Solution 1 (requires a new writable field {field_name}_id)

A possible solution is to set read_only=True in your ClassSerializer, and use an alternative name for the field when writing, it's common to use {field_name}_id. That way, the validation won't be done. See this answer for more details.

class StreamSerializer(serializers.ModelSerializer):
  classes = ClassSerializer(read_only=True)

  class Meta:
    model = Stream
    fields = (
      'pk',
      'stream_name',
      'classes',
      'created_date',
      'classes_id',
    )
    extra_kwargs = {
      'classes_id': {'source': 'classes', 'write_only': True},
    }

This is a clean solution but requires changing the user API. In case that's not an option, proceed to the next solution.

Solution 2 (requires overriding to_internal_value)

Here we override the to_internal_value method. This is where the nested ClassSerializer is throwing the error. To avoid this, we set that field to read_only and manage the validation and parsing in the method.

Note that since we're not declaring a classes field in the writable representation, the default action of super().to_internal_value is to ignore the value from the dictionary.

from rest_framework.exceptions import ValidationError


class StreamSerializer(serializers.ModelSerializer):
  classes = ClassSerializer(read_only=True)

  def to_internal_value(self, data):
      classes_pk = data.get('classes')
      internal_data = super().to_internal_value(data)
      try:
        classes = Classes.objects.get(pk=classes_pk)
      except Classes.DoesNotExist:
          raise ValidationError(
            {'classes': ['Invalid classes primary key']},
            code='invalid',
          )
      internal_data['classes'] = classes
      return internal_data

  class Meta:
    model = Stream
    fields = (
      'pk',
      'stream_name',
      'classes',
      'created_date',
    )

With this solution you can use the same field name for both reading and writing, but the code is a bit messy.

Additional notes

  • You're using the related_name argument incorrectly, see this question. It's the other way around,
classes = models.ForeignKey(
  Classes,
  related_name='streams',
  on_delete=models.CASCADE,
)

In this case it should be streams.

Upvotes: 11

Related Questions