6

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

3 Answers 3

11

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.

Sign up to request clarification or add additional context in comments.

2 Comments

I have tried this and it is still displaying an error {"classes_id":["This field is required."]}
@Andrew Just updated the answer to include a method that doesn't require a change of field name
1

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.

Comments

0

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)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.