Django REST Framework POST nested objects

You are dealing with the problem of nested serialization. Please read the linked documentation before proceeding.

Your question relates to a complex area of problems in DRF and hence requires some explanation and discussion for understanding how serializers and viewsets work.

I will discuss the problem of representing your Subject and Class data via the same endpoint by using a different representation of data for different HTTP methods, because this is commonly the problem when people wish to represent their data in nested formats; they wish to provide their user interfaces enough information for clean use, e.g. through the dropdown selectors.

By default Django and Django REST Framework (DRF) refer to related objects (your Subject and Class) by their primary keys. These, by default, are auto-incrementing integer keys with Django. If you want to refer to them by other ways you have to write overrides for this. There are a few different options.

  1. First option is to specialize your creation and update logic: Refer to your class via some other attribute(s) and manually write the lookups for creation yourself, or set the key you are referring through as the primary key of your class. You can set your class' name, UUID or any other attribute as the primary database key, as long as it is a unique, single field (the reason I am mentioning this is because you are, at the moment, looking your Class models up with a composite search that consists of a composite (number, letter) search term). You can override related object lookups in your create view method (for POST), for example, but then you will have to handle similar lookups in your update view method as well (for PUT and PATCH).
  2. Second, in my opinion the preferable option, is to specialize your object representations: Refer to your classes normally via primary key and create one serializer for reading the object and one for creating and updating it. This can be easily achieved by serializer class inheritance and overriding your representations. Use the primary key in your POST, PUT, PATCH, etc. requests to update your class references and foreign keys.

Option 1: Look Class and Subject up with an arbitrary attribute in create and update:

Set your nested class serializers as read-only:

class ExamSerializer(serializers.ModelSerializer):
    subject = SubjectSerializer(read_only=True)
    clazz = ClassSerializer(read_only=True)

Override your view's create to look up the related classes on free-form attributes. Also, check out how DRF implements this with mixins. You will also have to override your update method to handle these correctly, and take into account PATCH (partial update) support in addition to PUT (update) if you take this route:

def create(self, request):
    # Look up objects by arbitrary attributes.
    # You can check here if your students are participating
    # the classes and have taken the subjects they sign up for.
    subject = get_object_or_404(Subject, title=request.data.get('subject'))
    clazz = get_object_or_404(
        Class, 
        number=request.data.get('clazz_number')
        letter=request.data.get('clazz_letter')
    )

    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    serializer.save(clazz=clazz, subject=subject)
    headers = self.get_success_headers(serializer.data)

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

Option 2: Specialize your serializers for read and write and use primary keys; This is the idiomatic approach:

First define a default ModelSerializer you wish to use for normal operations (POST, PUT, PATCH):

class ExamSerializer(serializers.ModelSerializer)
    class Meta:
        model = Exam
        fields = ('id', 'subject', 'clazz', 'topic', 'date', 'details')

Then override the necessary fields with the kind of representation you want to give to them for reading the data (GET):

class ExamReadSerializer(ExamSerializer):
     subject = SubjectSerializer(read_only=True)
     clazz = ClassSerializer(read_only=True)

Then specify the serializer you wish to use for different operations for your ViewSet. Here we return the nested Subject and Class data for read operations, but only use their primary keys for update operations (far simpler):

class ExamViewSet(viewsets.ModelViewSet):
     queryset = Exam.objects.all()

     def get_serializer_class(self):
         # Define your HTTP method-to-serializer mapping freely.
         # This also works with CoreAPI and Swagger documentation,
         # which produces clean and readable API documentation,
         # so I have chosen to believe this is the way the
         # Django REST Framework author intended things to work:
         if self.request.method in ['GET']:
             # Since the ReadSerializer does nested lookups
             # in multiple tables, only use it when necessary
             return ExamReadSerializer
         return ExamSerializer

As you can see, option 2 seems fairly less complex and error-prone, containing only 3 lines of hand-written code on top of DRF (the get_serializer_class implementation). Just let the framework's logic figure out the representations and creation and updates of objects for you.

I have seen many other approaches, but so far these have been the ones that have produced the least code to maintain for me and take advantage of the design of DRF in a clean manner.


An easier approach without doing any additional classes is to take serialization on yourself:

class ExamSerializer(serializers.ModelSerializer):
    class Meta:
        model = Exam
        fields = ('id', 'subject', 'clazz', 'topic', 'date', 'details')

    def to_representation(self, instance):
        data = super().to_representation(instance)
        data['subject'] = SubjectSerializer(
            Subject.objects.get(pk=data['subject'])).data
        data['clazz'] = ClassSerializer(
            Class.objects.get(pk=data['clazz'])).data
        return data