Reputation: 5958
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."
.
Upvotes: 51
Views: 48163
Reputation: 191
I think SerializerMethodField is much more simple.
It looks like @validname' solution but that much more readable.
class BlogSerializer(serializers.ModelSerializer):
writer = serializers.SerializerMethodField()
comments = serializers.SerializerMethodField()
class Meta:
model = Blog
fields = '__all__'
def get_comments(self, obj):
return CommentSerializer(obj.comments.all(), many=True).data
def get_writer(self, obj):
return WriterSerializer(instance=obj.writer).data
Upvotes: 0
Reputation: 1840
To solve your problem you can use this package drf-rw-serializers
All you need to do is use two serializers (one to read and one to write):
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')
class WriteExamSerializer(serializers.ModelSerializer):
subject = SubjectSerializer()
clazz = ClassSerializer()
class Meta:
model = Exam
fields = ('id', 'subject', 'clazz', 'topic', 'date', 'details')
def create(self, validated_data):
subject = validated_data.pop('subject', None)
# logic to update subject
clazz = validated_data.pop('clazz', None)
# logic to update clazz
return super().create(validated_data)
def update(self, validated_data):
subject = validated_data.pop('subject', None)
# logic to update subject
clazz = validated_data.pop('clazz', None)
# logic to update clazz
return super().update(validated_data)
from drf_rw_serializers import generics
from .models import Exam
from .serializers import WriteExamSerializer, ExamSerializer
class ExamListCreateView(generics.ListCreateAPIView):
queryset = Exam.objects.all()
write_serializer_class = WriteExamSerializer
read_serializer_class = ReadExamSerializer
Upvotes: 3
Reputation: 1451
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.
Upvotes: 78
Reputation: 1526
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
Upvotes: 17
Reputation: 51
I had the same issue when trying to post a nested JSON object to DRF (Django Rest Framework).
Once you've properly set up writing nested serializers (see the docs on writable nested serializers), you can test that it works by using the browsable API and posting/putting data there. If that works, and you're still getting "This field is required" errors on your nested models when posting/putting JSON objects, you may have to set the content-type of your request.
This answer provided the solution I needed, and it's summarised below.
$.ajax ({
// Other parameters e.g. url, type
data: JSON.stringify(data),
dataType: "json",
contentType: "application/json; charset=utf-8",
});
I needed to set the "contentType" as well as "stringify" my js object.
Upvotes: 1