I wanted to have a serializer which would read as an object of fields on a related models, but save as a pk value to the related object. Something like a nested model with depth=1, but with more flexibility on the nested model. Essentialy I wanted the following query structure
GET child/{id}
{
"id": 1,
"name": "child",
"parent": {
"id": 1,
"name": "parent"
}
}
POST child/{id}
{
"id": 1,
"name": "child",
"parent": 1
}
So I wrote the following serializers:
# models.py
class Parent(models.Model):
name = models.CharField(max_length=255)
class Child(models.Model):
name = models.CharField(max_length=255)
parent = models.ForeignKey(Parent)
# serializers.py
class ParentSerializer(serializers.ModelSerializer):
"""Write as pk, read as object"""
class Meta:
model = models.Parent
fields = ('id', 'name')
def to_internal_value(self, data):
return self.Meta.model.objects.get(pk=data)
class ChildSerializer(serializers.ModelSerializer):
parent = ParentSerializer()
class Meta:
model = models.Child
fields = ('id', 'name', 'parent')
This almost works as expected, but the child serializer doesn't work when passed a querydict. To illustrate the problem:
# This works great! everything as expected
parent = Parent.objects.create(name='parent')
data = {'name': 'child', 'parent': parent.pk}
serializer = ChildSerializer(None, data=data)
serializer.is_valid()
serializer.save()
# This borks
data = QueryDict('name={0}&parent={1}'.format('child', parent.pk))
serializer = ChildSerializer(None, data=data)
serializer.is_valid()
serializer.save()
TypeError: int() argument must be a string, a bytes-like object or a number, not 'dict'
As a workaround I've forced ChildSerializer to convert QueryDicts to normal dictionaries, but I'd still like to know why the above fails and if there is a better option for this kind of API structure.
Related
I'm trying to create a nested serializer, UserLoginSerializer , composed of a UserSerializer and a NotificationSerializer, but I'm getting this error when it tries to serialize:
AttributeError: Got AttributeError when attempting to get a value for
field email on serializer UserSerializer. The serializer field
might be named incorrectly and not match any attribute or key on the
UserSerializer instance. Original exception text was:
'UserSerializer' object has no attribute 'email'.
Here is my models.py:
class Notification(models.Model):
kind = models.IntegerField(default=0)
message = models.CharField(max_length=256)
class User(AbstractUser):
username = models.CharField(max_length=150, blank=True, null=True)
email = models.EmailField(unique=True)
customer_id = models.UUIDField(default=uuid.uuid4, editable=False)
And my serializers.py:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
"id",
"first_name",
"last_name",
"email",
"customer_id"
]
class NotificationSerializer(serializers.ModelSerializer):
class Meta:
model = Notification
fields = [
"id",
"kind",
"message",
]
class UserLoginSerializer(serializers.Serializer):
user_info = UserSerializer(read_only=True)
notifications = NotificationSerializer(many=True, read_only=True)
The error occurs at the last line in this endpoint:
def get_login_info(self, request):
notifications = Notification.objects.filter(recipient=request.user)
serializer = UserLoginSerializer(
{
"user_info": UserSerializer(request.user),
"notifications": NotificationSerializer(notifications, many=True),
}
)
return Response(serializer.data)
What am I doing wrong?
You can use .data attribute.
def get_login_info(self, request):
notifications = Notification.objects.filter(recipient=request.user)
serializer = UserLoginSerializer(
{
"user_info": UserSerializer(request.user).data,
"notifications": NotificationSerializer(notifications, many=True).data,
}
)
return Response(serializer.data)
You must pass the data, not the serializer objects themselves. The data argument allows you to pass in a dictionary of data that will be used by the inner serializers to create a serialized representation. In this case, you will pass in the serialized data from the UserSerializer and NotificationSerializer to the UserLoginSerializer, which then returns the final serialized representation of the data.
Or, you may pass user and notifications directly as such:
def get_login_info(self, request):
notifications = Notification.objects.filter(recipient=request.user)
serializer = UserLoginSerializer(
{
"user_info": request.user,
"notifications": notifications
}
)
return Response(serializer.data)
Django Rest Framework model serializers have a to_representation method which converts the model instances to their dictionary representation, so in this case, the UserLoginSerializer will automatically use the UserSerializer and NotificationSerializer to serialize the user and notifications data. You can modify/change this method's behaviour by overriding it.
I hope this helps.
I have a serializer that gives this data
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ('id', 'name', 'cost', 'currency')
class UserSerializer(serializers.ModelSerializer):
posts = PostSerializer(many=True)
class Meta:
model = User
fields = ('id', 'name')
and it gives the response like this,
{
"id": 1,
"name": "joe",
"posts": [
{
"id": 20,
"name": "first post",
"cost": 20.00,
"currency": "USD"
},
{
"id": 21,
"name": "second post",
"cost": 30.00,
"currency": "USD"
}
]
}
However I want to change/add the fields of the response based on few conditions,
Eg. if cost is less than 25, make it zero and add a discount field for every post.
This is how I am doing.
class MyPostView(APIView):
def get(request):
query_set = User.objects.all()
user_and_posts = UserSerializer(query_set)
response_data = user_and_posts.data
# I am modifying the serializer data here :<
for post in response_data['posts']:
post['discount'] = 10 # some value
if post['cost'] < 25:
post['cost'] = 0
return Response(serializer.data, status=status.HTTP_200_OK)
To me modifying the primitive data like this is not looking right,
is there any alternate way in django rest to do this?
or could've done better with serializer?
In general, what's the best way to alter the response data we get from serializer and
format it in the way client wants? In languages like Java, we will have serializer for model and another serializer for output.. Can I do something similar?
If it is something that is model related and can be derived by manipulating model variables, I would advise to add properties into your model
class Post(models.Model):
name = models.CharField(max_length=64)
cost = models.DecimalField(max_digits=8, decimal_places=2)
currency = models.CharField(max_length=3)
#property
def alternative_cost(self):
if self.cost < 25:
return 0
else:
return self.cost
Then you have to add the newly created property to serializer.
Hello I have the following structure:
class Category(models.Model):
model.py
"""Class to represent the category of an Item. Like plants, bikes..."""
name = models.TextField()
description = models.TextField(null=True)
color = models.TextField(null=True)
# This will help to anidate categories
parent_category = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
null=True,
)
Then I serialize it:
serializers.py:
class CategorySerializer(serializers.ModelSerializer):
"""Serializer for Category."""
class Meta: # pylint: disable=too-few-public-methods
"""Class to represent metadata of the object."""
model = Category
fields = ['id', 'name', 'description', 'color', 'parent_category']
And I Create my endpint
views.py:
class CategoryViewset(viewsets.ModelViewSet): # pylint: disable=too-many-ancestors
"""API Endpoint to return the list of categories"""
queryset = Category.objects.all()
serializer_class = CategorySerializer
pagination_class = None
Well this seems to work as expected to make a post request, for example sending this:
{
"name": "Plants",
"description": null,
"color": "#ef240d",
"parent_category": 1
}
But when I make a request of this I want to see the parent category and not have to do two requests. So I found from other questions that I could use an external library:
serializer.py
from rest_framework_recursive.fields import RecursiveField
class CategorySerializer(serializers.ModelSerializer):
"""Serializer for Category."""
parent_category = RecursiveField(many=False)
class Meta: # pylint: disable=too-few-public-methods
"""Class to represent metadata of the object."""
model = Category
fields = ['id', 'name', 'description', 'color', 'parent_category', 'category_name']
And then It seems to work:
{
"id": 2,
"name": "Flowers",
"description": null,
"color": "#ef240a",
"parent_category": {
"id": 1,
"name": "Plants",
"description": "something",
"color": "#26def2",
"parent_category": null,
},
},
But when I try to post now it will not work as It seems to expect an object instead of just the ID which is what I would have available in my frontend:
{
"parent_category": {
"non_field_errors": [
"Invalid data. Expected a dictionary, but got int."
]
}
}
Is it possible to mix somehow this two approaches in my ModelSerializer?
You can customise your ModelViewSet to use two serializers instead of one. For example
class CategoryViewset(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
pagination_class = None
def create(self, request):
new_category = CategoryCreateSerializer(data=request.data)
if new_category.is_valid:
return Response(CategoryRetrieveSerializer(new_category).data)
I'm trying while creating object A, create object B, which are in many-to-many relation. I've read about it for some time already and I've created custom create() method in serializer to create associated B objects while creating A object, but there's no trace of B content in validated_data.
My models:
class A(models.Model):
name = models.CharField('Name', max_length=250)
description = models.TextField('Content')
timestamp = models.DateTimeField('Timestamp', auto_now_add=True)
b_field = models.ManyToManyField(B, blank=True, null=True, verbose_name='b', related_name='a')
class B(models.Model):
name = models.CharField('Name', max_length=250)
description = models.TextField('Description')
timestamp = models.DateTimeField('Timestamp', auto_now_add=True)
And A serializer:
class ASerializer(serializers.ModelSerializer):
b = BSerializer(many=True, required=False)
class Meta:
model = A
fields = '__all__'
read_only_fields = ['pk', 'timestamp']
def create(self, validated_data):
bs = validated_data.pop('b')
a = A.objects.create(**validated_data)
for b in bs:
B.objects.create(**b)
return a
I've been printing content of validated_data at the begginging of create() method and it looked like this:
{'name': 'test', 'description': 'none'}
So, no trace of B content. I've been testing with postman, sending something like this:
{
"name": "test",
"description": "none",
"b": [
{
"id": 1,
"name": "b test",
"description": "none b",
}
]
}
Okay, so I managed to solve it by myself (almost). drf-writtable-nested to the rescue.
First of, I changed my A serializer so it now inherits after WritableNestedModelSerializer from package mentioned above. Thanks to this, content of b is now visible in validated_data passed to create() method. Speaking of which, from what I have read (and what can be seen in the question itself), there's a need to create m2m objects in overwritten create() method. Mine looks like this now:
def create(self, validated_data):
bs_data = validated_data.pop('b')
a = self.Meta.model.objects.create(**validated_data)
for b_data in bs_data:
b = B.objects.create(**b_data)
a.b.add(b)
return a
There's still place to validate B instance in this serializer, but let's say, this solves my issue.
Related to this Topic
Hi,
I cannot follow the answer at the attached topic, because an ID is missing after serialization.
Model.py
class Owner(models.Model):
name = models.CharField(db_index=True, max_length=200)
class Car(models.Model):
name = models.CharField(db_index=True, max_length=200)
LCVS = models.ForeignKey(Owner)
View.py
class OwnerViewSet(viewsets.ModelViewSet):
queryset = Owner.objects.all()
serializer_class = OwnerSerializer
class CarViewSet(viewsets.ModelViewSet):
serializer_class = CarSerializer
queryset = Car.objects.all()
Serializer.py
class OwnerSerializer(serializers.ModelSerializer):
class Meta:
model = Owner
fields = ('id', 'name')
class CarSerializer(serializers.ModelSerializer):
owner = OwnerSerializer()
class Meta:
model = Car
fields = ('id', 'name', 'owner')
def create(self, validated_data):
tmp_owner = Owner.objects.get(id=validated_data["car"]["id"])
car = Car.objects.create(name=self.data['name'],owner=tmp_owner)
return car
Now i send the following request :
Request URL:http://localhost:9000/api/v1/cars
Request Method:POST
Request Paylod :
{
"name": "Car_test",
"ower": {
"id":1,
"name": "Owner_test"
}
}
But, here the validated_data don't contain the owner ID !
Traceback | Local vars
validated_data {u'Owner': OrderedDict([(u'name', u'Owner_test')]), u'name': u'Car_test'}
#Kevin Brown :
Workful ! Thanks
I'll validate your answer but I get a new problem...
Now when I try to put a new Owner, an error raise :
{
"id": [
"This field is required."
]
}
I had to create a new serializer ?
Any AutoFields on your model (which is what the automatically generated id key is) are set to read-only by default when Django REST Framework is creating fields in the background. You can confirm this by doing
repr(CarSerializer())
and seeing the field generated with read_only=True set. You can override this with the extra_kwargs Meta option which will allow you to override it and set read_only=False.
class OwnerSerializer(serializers.ModelSerializer):
class Meta:
model = Owner
fields = ('id', 'name')
extra_kwargs = {
"id": {
"read_only": False,
"required": False,
},
}
This will include the id field in the validated_data when you need it.