I'm facing a little problem right now with Django Rest Framework. I'm trying to post an object with nested objects in it.
Here are my serializers.py
:
class ClassSerializer(serializers.ModelSerializer): class Meta: model = Class fields = ('number', 'letter') class SubjectSerializer(serializers.ModelSerializer): class Meta: model = Subject fields = ('title',) class ExamSerializer(serializers.ModelSerializer): subject = SubjectSerializer() clazz = ClassSerializer() class Meta: model = Exam fields = ('id', 'subject', 'clazz', 'topic', 'date', 'details') depth = 1 def create(self, validated_data): return Exam.objects.create(**validated_data) def update(self, instance, validated_data): instance.__dict__.update(**validated_data) instance.save() return instance
And create()
from views.py
:
def create(self, request): serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) return Response(serializer.validated_data, status=status.HTTP_201_CREATED)
And here it is the response from Postman:
I've read some posts here about this problem but I'm still stuck with it. I've tried to fix it in several ways but it is still returning "This field is required."
.
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.
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). 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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With