How to deserialize nested objects with Django Rest Framework - json

Say I have Django models like this:
class Book(models.Model):
title = models.CharField(max_length=150)
author = models.CharField(max_length=150)
class Chapter(models.Model):
book = models.ForeignKey(Book, related_name='chapters')
title = models.CharField(max_length=150)
page_num = models.IntegerField()
and Django Rest Framework classes like this:
class ChapterSerializer(serializers.ModelSerializer):
class Meta:
model = Chapter
fields = ('id', 'title', 'page_num')
class BookSerializer(serializers.ModelSerializer):
chapters = ChapterSerializer(many=True)
class Meta:
model = Book
fields = ('id', 'title', 'author', 'chapters')
def create(validated_data):
chapters = validated_data.pop('chapters')
book = Book(**validated_data)
book.save()
serializer = ChapterSerializer(data=chapters, many=True)
if serializer.is_valid(raise_exception=True):
chapters = serializer.save()
class BookCreate(generics.CreateAPIView):
serializer = BookSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
serializer.save()
# Do some other stuff
and I POST some JSON like this:
{
title: "Test book",
author: "Test author",
chapters: [
{title: "Test chapter 1", page_num: 1},
{title: "Test chapter 2", page_num: 5}
]
}
I get an exception because chapter doesn't have a book associated with it. If I add book as one of the fields of ChapterSerializer, then the JSON will fail to validate because the BookSerializer in BookCreate won't validate because it will expect a book id for the chapters, but the book hasn't been created yet. How do I resolve this situation?
Is there a way to have the BookSerializer to validate its own fields and not to validate its chapter's?

You can pass additional arguments on .save. So I think you just need to pass the newly created book instance to the serializer, e.g.
def create(validated_data):
chapters = validated_data.pop('chapters')
book = Book(**validated_data)
book.save()
serializer = ChapterSerializer(data=chapters, many=True)
if serializer.is_valid(raise_exception=True):
chapters = serializer.save(book=book)

I think, you should not create another Serializer inside the create()method of a serializer because this is redundant. The validation has already been done by the serializer if you defined it to be the serializer for the model referenced by this field:
class BookSerializer(serializers.ModelSerializer):
# here you define the serializer to validate your input
chapters = ChapterSerializer(many=True)
instead you can just create the object, the data has already been validated by the call to is_valid() of the initial serializer. you need to pass the book to the create()method anyways:
def create(validated_data):
chapters_data = validated_data.pop('chapters')
book = Book.objects.create(**validated_data)
for chapter_data in chapters_data:
Chapter.objects.create(book=book, **chapter_data)

Related

Adding Additional Data to a Serialize Response in Django

Updated
I changed my simplified question into a real example.
I've created a working post response of data from the model using ModelSerialzer, which I call from a post method in a view class. I would like to add additional data to the response. This is the pertinent code from my CBV:
def post(self, request, format=None):
user_profile = UserProfiles.objects.get(user=request.user.id)
service_id = user_profile.service_id
rec_filter = Recommendations.objects.values_list('resource')
if service_id > 0:
service_name = Services.objects.get(pk=service_id)
programs = Programs.objects.filter(services=service_id)
resources_filtered = Resources.objects.filter(program__in=programs).exclude(id__in=rec_filter)
else:
service_name = 'All Services'
resources_filtered = Resources.objects.exclude(id__in=rec_filter)
serializer = ResourceSerializer(resources_filtered, many=True)
#serializer.data["service"] = service_name
return Response(serializer.data)
The commented out line was my attempt to add data base on a similar post here. I get a 500 response in my API call. What is the correct way to do it? The response data is JSON if that's necessary to mention.
This is the ModelSerializer:
class ResourceSerializer(serializers.ModelSerializer):
organization = OrganizationSerializer(read_only=True)
program = ProgramSerializer(read_only=True)
class Meta:
model = Resources
fields = [
'organization',
'program',
'link',
'contact',
'general_contact',
'eligibility',
'service_detail'
]
Test of the answer
Heres the updated code based on the answer with a correction to fix and error:
class ResourceSerializer(serializers.ModelSerializer):
organization = OrganizationSerializer(read_only=True)
program = ProgramSerializer(read_only=True)
service = serializers.SerializerMethodField()
def get_service(self, obj):
return "All Services"
class Meta:
model = Resources
fields = [
'organization',
'program',
'link',
'contact',
'general_contact',
'eligibility',
'service_detail',
'service'
]
The problem with this approach is that the value "All Services" is repeated in every row serialized. It's only needed once. I'd also like to keep the data transmitted minimized.
The problem with the original attempt is that serializer.data is immutable. It's necessary to make a copy and add to it.
serializer = ResourceSerializer(resources_filtered, many=True)
augmented_serializer_data = list(serializer.data)
augmented_serializer_data.append({'service': 'All Services'})
return Response(augmented_serializer_data)
This answer is based on one given by #andre-machado in this question.
This code here is an example to coincide with the other answer given.
You can do it in serializer itself. Define the new field required and add it in fields. Mark all the fields in serializer from resource model.
class ResourceSerializer(serializers.ModelSerializer):
service = serializers.SerializerMethodField()
def get_service(self):
return "All Services"
class Meta :
model = Resources
fields = ('service') #Mark all the fields required here from resource model
You can do it from the serilaizer. In this case i was adding the field isOpen to the response and this is how i did it .timeDifference is the name of the function that was to generate data for the extra field . I hope it helps
class ShopSearchSerializer(serializers.ModelSerializer):
isOpen = serializers.SerializerMethodField('timeDifference')
def timeDifference(self,*args):
requestTime = datetime.now()
return requestTime
class Meta:
model = Shop
fields =['name','city','street','house','opening_time','closing_time','isOpen']

Why is my Django serializer telling me an attribute doesn't exist when I see it defined in my model?

I'm using Python 3.7 and the Django rest framework to serialize some models into JSOn data. I have this
data = {
'articlestats': ArticleStatSerializer(articlestats, many=True).data,
and then I have defined the following serializers ...
class LabelSerializer(serializers.ModelSerializer):
class Meta:
model = Label
fields = ['name']
...
class ArticleSerializer(serializers.ModelSerializer):
label = LabelSerializer()
class Meta:
model = Article
fields = ['id', 'title', 'path', 'url', 'label']
class ArticleStatSerializer(serializers.ModelSerializer):
article = ArticleSerializer()
class Meta:
model = ArticleStat
fields = ['id', 'article', 'score']
I have defined my Label model like so ...
class Label(models.Model):
name = models.CharField(max_length=200)
def __str__(self):
return self.name
class Meta:
unique_together = ("name",)
but I'm getting this error when Django processes my serialize line ...
AttributeError: Got AttributeError when attempting to get a value for field `name` on serializer `LabelSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `str` instance.
Original exception text was: 'str' object has no attribute 'name'.
Not sure why it's complaining. The "name" attribute is right there. What else should I be doing?
Edit: Models asked for ...
class ArticleStat(models.Model):
objects = ArticleStatManager()
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='articlestats')
score = models.FloatField(default=0, null=False)
class Article(models.Model):
objects = ArticleManager()
title = models.TextField(default='', null=False)
path = models.TextField(default='', null=False)
url = models.TextField(default='', null=False)
label = models.TextField(default='', null=True)
created_on = models.DateTimeField(db_index=True, default=datetime.now)
In Article model the label field is TextField, not any kind of related fields. But in your serializer, it expects a related field.
class ArticleSerializer(serializers.ModelSerializer):
label = LabelSerializer()
class Meta:
model = Article
fields = ['id', 'title', 'path', 'url', 'label']

Object of type "" is not JSON serializable

I am trying to create an API and pass the context data in Response but I am getting the error:
Object of type TakenQuiz is not JSON serializable
Below is the code:
taken_quizzes = quiz.taken_quizzes.select_related('supplier__user').order_by('-date')
total_taken_quizzes = taken_quizzes.count()
quiz_score = quiz.taken_quizzes.aggregate(average_score=Avg('score'))
least_bid = quiz.taken_quizzes.aggregate(least_bid=Min('least_bid'))
extra_context = {'taken_quizzes': taken_quizzes,
'total_taken_quizzes': total_taken_quizzes,
'quiz_score': quiz_score, 'least_bid': least_bid, 'matching_bids': matching_bids,
'driver_num': driver_num, 'lat_lon_orig': lat_lon_orig, 'lat_lon_dest': lat_lon_dest,
'user_pass': user_pass, 'username': username, 'password': password, }
print("extra content is ", extra_context)
return Response(extra_context)
Here is the context data:
extra content is {'taken_quizzes': <QuerySet [<TakenQuiz: TakenQuiz object (1)>]>, 'total_taken_quizzes': 1, 'quiz_score': {'average_score': 0.0}, 'least_bid': {'least_bid': 899}, 'matching_bids': [], 'driver_
num': 0, 'lat_lon_orig': '36.1629343, -95.9913076', 'lat_lon_dest': '36.1629343, -95.9913076', 'user_pass': ('jkcekc', 'efce'), 'username': 'efw', 'password': 'sdf'}
The error I believe is because of the queryset in the extra_context, How do I resolve this ?
I tried json.dumps but it still doesn't work
Serializer.py
class takenquizSerializer(serializers.ModelSerializer):
class Meta:
model = TakenQuiz
fields = "__all__"
Models.py
class TakenQuiz(models.Model):
supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE, related_name='taken_quizzes')
quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, related_name='taken_quizzes')
score = models.FloatField()
date = models.DateTimeField(auto_now_add=True)
least_bid = models.IntegerField(default=0)
confirmed = models.CharField(max_length=100, default='Not Confirmed')
UPDATE
taken_quizzes = quiz.taken_quizzes.select_related('supplier__user').order_by('-date')
taken_quizzs = takenquizSerializer(taken_quizzes).data
You need to serialize taken_quizzes objects either via some serializer or calling
".values()" and specifying the required key, if any (otherwise it will give all values of the model as a dictionary)
{
'taken_quizes': TakenQuizSerializer(taken_quizzes, many=True).data,
# or
'taken_quizzes': taken_quizzes.values(),
....
}
as ruhaib mentioned you need to serialize the data. If i dont want to define special serializers for models this is what I do.
from django.core import serializers
taken_quizzes=....
data=serializers.serialize('json',taken_quizzes)
you can do this before you populate extra_content with some data.

Django Rest Framework: Deserializing and get the primary key from validated_data

I defined a nested model Product as follow. Each Product can belong to a lot of Productlist.
class Product(models.Model):
product_id = models.AutoField(primary_key=True)
product_name = models.CharField(max_length=50)
class Productlist(models.Model):
productlist_id = models.AutoField(primary_key=True)
productlist_name = models.CharField(max_length=50)
product = models.ManyToManyField(Product, related_name='productlists')
The corresponding serializers are:
class ProductlistSerializer(serializers.ModelSerializer):
class Meta:
model = Productlist
fields = ('productlist_id', 'productlist_name',)
class ProductSerializer(serializers.ModelSerializer):
productlists = ProductlistSerializer(many=True, required=False)
class Meta:
model = Product
fields = ('product_id', 'product_name', 'product lists')
def create(self, validated_data):
#some codes
When I POST a new Product (url(r'^api/products/$', views.ProductEnum.as_view()), I would like to update the product lists for adding the new product to the corresponding product lists. The JSON file I prefer to use is:
{
"product_name": "product1"
"productlist": [
{
"productlist_id": 1,
"productlist_name": "list1",
},
{
"productlist_id": 2,
"productlist_name": list2"
}
]
}
The problem is that I cannot get the productlist_id from validated_data. In Django Rest Framework, you always need to call to_internal_value() for deserializing data and generate validated_data. After some degugging, I checked the code of DRF and find the following snippets in to_internal_value():
def to_internal_value(self, data):
"""
Dict of native values <- Dict of primitive datatypes.
"""
if not isinstance(data, dict):
message = self.error_messages['invalid'].format(
datatype=type(data).__name__
)
raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [message]
})
ret = OrderedDict()
errors = OrderedDict()
fields = [
field for field in self.fields.values()
if (not field.read_only) or (field.default is not empty)
]
for field in fields:
validate_method = getattr(self, 'validate_' + field.field_name, None)
primitive_value = field.get_value(data)
try:
validated_value = field.run_validation(primitive_value)
if validate_method is not None:
validated_value = validate_method(validated_value)
except ValidationError as exc:
errors[field.field_name] = exc.detail
except DjangoValidationError as exc:
errors[field.field_name] = list(exc.messages)
except SkipField:
pass
else:
set_value(ret, field.source_attrs, validated_value)
if errors:
raise ValidationError(errors)
return ret
Please notice the to_internal_value's fields has ignored the IntegerField(read_only=True) for it cannot satisfy the following condition:
fields = [
field for field in self.fields.values()
if (not field.read_only) or (field.default is not empty)
]
So the validated_data will just have the following data:
{
"product_name": "product1"
"productlist": [
{
"productlist_name": "list1",
},
{
"productlist_name": list2"
}
]
}
How could I get the primary key of product list? Thanks in advance!
After some digging, I found that the read_only fields are only for output presentation. You can find the similar question on the offcial github link of Django REST Framework.
So the solution is overriding the read_only field in the serializer as follow:
class ProductlistSerializer(serializers.ModelSerializer):
productlist_id = serializers.IntegerField(read_only=False)
class Meta:
model = Productlist
fields = ('productlist_id', 'productlist_name',)

json django model with custom fields representation

I have the following model:
class Messages(models.Model):
userid = models.ForeignKey(User)
time = models.TimeField(default=datetime.datetime.now)
content = models.CharField(max_length=255)
id = models.AutoField(primary_key=True)
I want to json this model with custom representation to pass it in javascript. I used the follow code for this by iterating through QuerySet
messages = Messages.objects.all()
passed_messages = []
for singleMess in messages:
passed_messages.append(get_message(singleMess))
response = json.dumps(passed_messages)
def get_message(message):
return {
'user': User.objects.get_by_natural_key(message.userid).username,
'content': message.content,
'hour': message.time.hour,
'minute': message.time.minute,
'id': message.id
}
But this part looks ugly. Is there a way to replace it with something nicer. I could use the followings
__repr__ in models.py
django.core.serializers
django.forms.models.model_to_dict
Messages.objects.all().values()
I just don't know how to come across.
For example
json.dumps needs a pure Python list of dictionaries, but __repr__ returns QuerySet which I don't know how to conrent.
If I use model_to_dict or serializers or list(messages.values()) I can't join the User's field username instead of userid.
I added property to my model class
class Messages(models.Model):
userid = models.ForeignKey(User)
time = models.TimeField(default=datetime.datetime.now)
content = models.CharField(max_length=255)
id = models.AutoField(primary_key=True)
#property
def json(self):
return {
'user': User.objects.get_by_natural_key(self.userid).username,
'content': self.content,
'time': self.time.strftime("%H:%M:%S"),
'id': self.id
}
And used comprehensions:
import json
messages = Messages.objects.all()
json.dumps([message.json for message in messages])