django-rest-framework 3.0 create or update in nested serializer
Solution 1
Firstly, do you want to support creating new book instances, or only updating existing ones?
If you only ever wanted to create new book instances you could do something like this...
class PageSerializer(serializers.Serializer):
text = serializers.CharField(max_length=500)
class BookSerializer(serializers.Serializer):
page = PageSerializer(many=True)
title = serializers.CharField(max_length=50)
def create(self, validated_data):
# Create the book instance
book = Book.objects.create(title=validated_data['title'])
# Create or update each page instance
for item in validated_data['pages']:
page = Page(id=item['page_id'], text=item['text'], book=book)
page.save()
return book
Note that I haven't included the book_id
here. When we're creating book instances we won't be including a book id. When we're updating book instances we'll typically include the book id as part of the URL, rather than in the request data.
If you want to support both create and update of book instances then you need to think about how you want to handle pages that are not included in the request, but are currently associated with the book instance.
You might choose to silently ignore those pages and leave them as they are, you might want to raise a validation error, or you might want to delete them.
Let's assume that you want to delete any pages not included in the request.
def create(self, validated_data):
# As before.
...
def update(self, instance, validated_data):
# Update the book instance
instance.title = validated_data['title']
instance.save()
# Delete any pages not included in the request
page_ids = [item['page_id'] for item in validated_data['pages']]
for page in instance.books:
if page.id not in page_ids:
page.delete()
# Create or update page instances that are in the request
for item in validated_data['pages']:
page = Page(id=item['page_id'], text=item['text'], book=instance)
page.save()
return instance
It's also possible that you might want to only support book updates, and not support creation, in which case, only include the update()
method.
There are also various ways you could reduce the number of queries eg. using bulk create/deletion, but the above would do the job in a fairly straightforward way.
As you can see there are subtleties in the types of behavior you might want when dealing with nested data, so think carefully about exactly what behavior you're expecting in various cases.
Also note that I've been using Serializer
in the above example rather than ModelSerializer
. In this case it's simpler just to include all the fields in the serializer class explicitly, rather than relying on the automatic set of fields that ModelSerializer
generates by default.
Solution 2
You can simply use drf-writable-nested. It automatically make your nested serializers writable and updatable.
in you serializers.py
:
from drf_writable_nested import WritableNestedModelSerializer
class RequestSerializer(WritableNestedModelSerializer):
book_id = serializers.IntegerField()
page = PageSerializer(many=True)
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
And that's it!
Also the library supports using only one of the create
and update
logics if you don't need both.
Sam R.
That which can be asserted without evidence, can be dismissed without evidence. - Christopher Hitchens (1949-2011) The avatar is called The Town of Thoughts by the magnificent Marija Tiurina.
Updated on April 30, 2020Comments
-
Sam R. about 4 years
With django-rest-framework 3.0 and having these simple models:
class Book(models.Model): title = models.CharField(max_length=50) class Page(models.Model): book = models.ForeignKey(Books, related_name='related_book') text = models.CharField(max_length=500)
And given this JSON request:
{ "book_id":1, "pages":[ { "page_id":2, "text":"loremipsum" }, { "page_id":4, "text":"loremipsum" } ] }
How can I write a nested serializer to process this JSON and for each
page
for the givenbook
either create a new page or update if it exists.class RequestSerializer(serializers.Serializer): book_id = serializers.IntegerField() page = PageSerializer(many=True) class PageSerializer(serializers.ModelSerializer): class Meta: model = Page
I know that instantiating the serializer with an
instance
will update the current one but how should I use it inside thecreate
method of nested serializer? -
Sam R. over 9 years
you might want to only support book updates ... , only include the update() method
. In this case, how theinstance
in update method will be filled with an existing book? -
Tom Christie over 9 yearsBy instantiating the serialiser with the 'instance' keyword argument. Normally you'd get that by performing a lookup based on a primary key in the URL. If you're using the generic views that'd be handled for you. Take a look at DetailMixin in 'mixins.py' for the implementation of that.
-
Sam R. over 9 yearsThanks Tom. I got it now.
-
CheapD AKA Ju over 9 yearsWhy the override is in the serializer and not in the view, like : django-rest-framework.org/api-guide/viewsets/… ?
-
JJD about 9 years@TomChristie Could you do me the favor and have a look at my attempt to create nested resources? I am out of ideas after trying django-rest-framework-nested-resource, drf-extensions and drf-nested-routers - all without success. I am happy to switch to something that actually works.
-
Seaux over 8 years@TomChristie rest-v3.2.5 is failing when I try to do
for page in instance.books
. The error says RelatedManager is not iterable. Is there a fix for this in a newer version? I used pip to install rest, so I feel like I'm using a pretty recent version. -
alfetopito over 8 years@Seaux try
for page in instance.books.all()
instead. Or even better, replaceall
withiterator
-
Sassan over 7 years@TomChristie If I use ModelSerializer instead of Serializer it filters out
page_id
. -
Ryu_hayabusa over 7 years@Sassan Just add an 'id' field in the related field serializer. In the OP's question , in the PageSerializer use "fields = ('id',...) in calss meta.
-
Ryu_hayabusa over 7 years@Sassan also
id = serializers.IntegerField()
-
Dilvane Zanardine about 7 yearsThis line
instance.title = validated_data['title']
is still needed? The .save() by it self will get all fields from validated_data, right? -
Sreekanth Reddy Balne over 5 years@TomChristie What if there is a
validate_<field>
on thePageSerializer
? With this approach validation won't be done on the Page object being created. Any Help? -
Alex78191 over 4 yearsHow to allow to create and edit nestet entity only by id?
-
Alex78191 over 4 yearsIn my case the anser is stackoverflow.com/questions/41394761/…
-
Agustín Lado about 4 yearsIf you have a lot of attributes to update in
Book
a nice way to do it is to dovalidated_data.pop("pages")
and deal with the pages yourself but let DRF deal with theBook
by callingsuper().update(instance, validated_data)
. -
Paul Schreiber over 3 years
page_ids = [item['page_id'] for item in validated_data['pages']]
this fails if one of the items is new and lacks an id.page_ids = [item['page_id'] for item in validated_data['pages'] if 'page_id' in item]
works around that.