DRF – AttributeError when using ModelSerializer to save manyToMany relation with 'though' table
Question:
I am building a Django Rest Framework project and I’m encountering an attribute error that I cannot seem to resolve. Specifically, I’m getting the following error when I try to serialize a Sale object:
Got AttributeError when attempting to get a value for field `product` on serializer `SaleItemSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Product` instance.
Original exception text was: 'Product' object has no attribute 'product'.
I’ve defined my models and serializers as follows:
# Models
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
cost = models.DecimalField(max_digits=10, decimal_places=2)
description = models.TextField()
image = models.ImageField(upload_to='images/')
category = models.ForeignKey(Category, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Sale(models.Model):
products = models.ManyToManyField(Product, related_name='sales', through='SaleItem')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class SaleItem(models.Model):
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Serializers
class SaleItemSerializer(serializers.ModelSerializer):
class Meta:
model = SaleItem
fields = ['product', 'quantity', 'value']
class SaleSerializer(serializers.ModelSerializer):
products = SaleItemSerializer(many=True)
class Meta:
model = Sale
fields = '__all__'
def create(self, validated_data):
sale_items_data = validated_data.pop('products')
sale = Sale.objects.create(**validated_data)
for sale_item in sale_items_data:
SaleItem.objects.create(sale=sale, **sale_item) # The erro
return sale
The code I’m using for the serializers is basically this documentation example: DRF Writable Nested Serializers the only difference is that SaleItem is an Through table for the manyToMany relation instead of a "normal" table.
I’ve tried changing the SaleItem object creation line to passing the data back to the serializer but that way the serializer as never valid and changing it to be valid for creation breaks the field for the SaleSerializer.
I suspect the issue is with the SaleItemSerializer or the SaleSerializer create method, but I cannot figure out what’s causing the attribute error.
The only hope I have right now is to write an normal Serializer and the whole logic but i really wanted to use the whole framework potential.
Answers:
Basically because you are trying to serialize a Product
using a SaleItem
model. And,Product
has no field called product
.
You can, for instance, add a related name to SaleItem
sales
field:
models.py
class SaleItem(models.Model):
sale = models.ForeignKey(
Sale,
on_delete=models.CASCADE,
related_name='items'
)
...
And, use this to access SaleItem
(and consequently the Product
) related with that Sale
instance together with SerializerMethodField:
class SaleItemSerializer(serializers.ModelSerializer):
class Meta:
model = SaleItem
fields = ['product', 'quantity', 'price']
class SaleSerializer(serializers.ModelSerializer):
products = serializers.SerializerMethodField()
class Meta:
model = Sale
fields = '__all__'
def get_products(self, instance):
qs = instance.items.all()
serializer = SaleItemSerializer(qs, many=True)
return serializer.data
Also, note that there is no value
field in SaleItem
, it is called price
.
Regarding creating objects, that will heavily depend on the payload you are sending, here is a simple example, assuming the products exists on the database and with no treatment otherwise:
payload
{
"sale_items":
[
{
"product": 1,
"quantity": 1,
"price": 123.12
},
{
"product": 2,
"quantity": 2,
"price": 234.23
},
{
"product": 1,
"quantity": 1,
"price": 456.45
}
]
}
Then we can override the create method on the ViewSet, and pass data via extra context to the serializer:
views.py
class SalesViewSet(ModelViewSet):
queryset = Sale.objects.all()
serializer_class = SaleSerializer
def create(self, request, *args, **kwargs):
sale_items = request.data.pop('sale_items')
serializer = self.get_serializer(data=request.data, context={'sale_items': sale_items})
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self. get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
serializers.py
class SaleSerializer(serializers.ModelSerializer):
...
def get_products(self, instance):
...
def create(self, validated_data):
sale_items = self.context.get('sale_items')
instance = Sale.objects.create(**validated_data)
for item in sale_items:
product_instance = Product.objects.get(id=item['product'])
item['product'] = product_instance
item['sale'] = instance
SaleItem.objects.create(**item)
return (instance)
Based on Niko’s answer I managed to get id done containing all the logic on the serializers.
As I stated in the comment and Niko after modified his answer to fit around, creation was root of the whole problem and SerializerMethodField is read_only so it couldn’t solve my problem completely, so I tried to mix up the two answers by creating an MethodField Subclass who’s not read_only, and it worked pretty well:
class ReadWriteSerializerMethodField(serializers.SerializerMethodField):
def __init__(self, method_name=None, **kwargs):
self.method_name = method_name
kwargs["source"] = "*"
super(serializers.SerializerMethodField, self).__init__(**kwargs)
def to_internal_value(self, data):
return {self.field_name: data}
With this we can still access the products data inside the create method, but now using the SaleItemSerializer itself to save the subitems
class SaleSerializer(serializers.ModelSerializer):
products = ReadWriteSerializerMethodField()
class Meta:
model = Sale
fields = "__all__"
def create(self, validated_data):
sale_items_data = validated_data.pop("products")
sale = Sale.objects.create(**validated_data)
for sale_item in sale_items_data:
sale_item["sale"] = sale.id
serializer = SaleItemSerializer(data=sale_item)
serializer.is_valid(raise_exception=True)
serializer.save()
return sale
def get_products(self, instance):
qs = instance.items.all()
serializer = SaleItemSerializer(qs, many=True)
return serializer.data
This way I was able to fix the whole issue and kept everything inside the serializers layer to easily add new things such as a size
parameter to the SaleItemSerializer
which is a write_only
param to specify the stock of the product which the quantity must be decreased as it is being sold.
I am building a Django Rest Framework project and I’m encountering an attribute error that I cannot seem to resolve. Specifically, I’m getting the following error when I try to serialize a Sale object:
Got AttributeError when attempting to get a value for field `product` on serializer `SaleItemSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Product` instance.
Original exception text was: 'Product' object has no attribute 'product'.
I’ve defined my models and serializers as follows:
# Models
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
cost = models.DecimalField(max_digits=10, decimal_places=2)
description = models.TextField()
image = models.ImageField(upload_to='images/')
category = models.ForeignKey(Category, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Sale(models.Model):
products = models.ManyToManyField(Product, related_name='sales', through='SaleItem')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class SaleItem(models.Model):
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Serializers
class SaleItemSerializer(serializers.ModelSerializer):
class Meta:
model = SaleItem
fields = ['product', 'quantity', 'value']
class SaleSerializer(serializers.ModelSerializer):
products = SaleItemSerializer(many=True)
class Meta:
model = Sale
fields = '__all__'
def create(self, validated_data):
sale_items_data = validated_data.pop('products')
sale = Sale.objects.create(**validated_data)
for sale_item in sale_items_data:
SaleItem.objects.create(sale=sale, **sale_item) # The erro
return sale
The code I’m using for the serializers is basically this documentation example: DRF Writable Nested Serializers the only difference is that SaleItem is an Through table for the manyToMany relation instead of a "normal" table.
I’ve tried changing the SaleItem object creation line to passing the data back to the serializer but that way the serializer as never valid and changing it to be valid for creation breaks the field for the SaleSerializer.
I suspect the issue is with the SaleItemSerializer or the SaleSerializer create method, but I cannot figure out what’s causing the attribute error.
The only hope I have right now is to write an normal Serializer and the whole logic but i really wanted to use the whole framework potential.
Basically because you are trying to serialize a Product
using a SaleItem
model. And,Product
has no field called product
.
You can, for instance, add a related name to SaleItem
sales
field:
models.py
class SaleItem(models.Model):
sale = models.ForeignKey(
Sale,
on_delete=models.CASCADE,
related_name='items'
)
...
And, use this to access SaleItem
(and consequently the Product
) related with that Sale
instance together with SerializerMethodField:
class SaleItemSerializer(serializers.ModelSerializer):
class Meta:
model = SaleItem
fields = ['product', 'quantity', 'price']
class SaleSerializer(serializers.ModelSerializer):
products = serializers.SerializerMethodField()
class Meta:
model = Sale
fields = '__all__'
def get_products(self, instance):
qs = instance.items.all()
serializer = SaleItemSerializer(qs, many=True)
return serializer.data
Also, note that there is no value
field in SaleItem
, it is called price
.
Regarding creating objects, that will heavily depend on the payload you are sending, here is a simple example, assuming the products exists on the database and with no treatment otherwise:
payload
{
"sale_items":
[
{
"product": 1,
"quantity": 1,
"price": 123.12
},
{
"product": 2,
"quantity": 2,
"price": 234.23
},
{
"product": 1,
"quantity": 1,
"price": 456.45
}
]
}
Then we can override the create method on the ViewSet, and pass data via extra context to the serializer:
views.py
class SalesViewSet(ModelViewSet):
queryset = Sale.objects.all()
serializer_class = SaleSerializer
def create(self, request, *args, **kwargs):
sale_items = request.data.pop('sale_items')
serializer = self.get_serializer(data=request.data, context={'sale_items': sale_items})
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self. get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
serializers.py
class SaleSerializer(serializers.ModelSerializer):
...
def get_products(self, instance):
...
def create(self, validated_data):
sale_items = self.context.get('sale_items')
instance = Sale.objects.create(**validated_data)
for item in sale_items:
product_instance = Product.objects.get(id=item['product'])
item['product'] = product_instance
item['sale'] = instance
SaleItem.objects.create(**item)
return (instance)
Based on Niko’s answer I managed to get id done containing all the logic on the serializers.
As I stated in the comment and Niko after modified his answer to fit around, creation was root of the whole problem and SerializerMethodField is read_only so it couldn’t solve my problem completely, so I tried to mix up the two answers by creating an MethodField Subclass who’s not read_only, and it worked pretty well:
class ReadWriteSerializerMethodField(serializers.SerializerMethodField):
def __init__(self, method_name=None, **kwargs):
self.method_name = method_name
kwargs["source"] = "*"
super(serializers.SerializerMethodField, self).__init__(**kwargs)
def to_internal_value(self, data):
return {self.field_name: data}
With this we can still access the products data inside the create method, but now using the SaleItemSerializer itself to save the subitems
class SaleSerializer(serializers.ModelSerializer):
products = ReadWriteSerializerMethodField()
class Meta:
model = Sale
fields = "__all__"
def create(self, validated_data):
sale_items_data = validated_data.pop("products")
sale = Sale.objects.create(**validated_data)
for sale_item in sale_items_data:
sale_item["sale"] = sale.id
serializer = SaleItemSerializer(data=sale_item)
serializer.is_valid(raise_exception=True)
serializer.save()
return sale
def get_products(self, instance):
qs = instance.items.all()
serializer = SaleItemSerializer(qs, many=True)
return serializer.data
This way I was able to fix the whole issue and kept everything inside the serializers layer to easily add new things such as a size
parameter to the SaleItemSerializer
which is a write_only
param to specify the stock of the product which the quantity must be decreased as it is being sold.