I have the following models:
class MenuItem(models.Model):
class Category(models.TextChoices):
PIZZA = "pizza"
SIDE = "side"
OTHER = "other"
name = models.CharField(max_length=40)
description = models.TextField(max_length=150)
price = models.FloatField(default=0.0)
category = models.CharField(max_length=50, choices=Category.choices, default=Category.OTHER)
class Order(models.Model):
class OrderStatus(models.TextChoices):
NEW = "new"
READY = "ready"
DELIVERED = "delivered"
customer = models.CharField(max_length=50)
order_time = models.DateTimeField(auto_now_add=True)
items = models.ManyToManyField(MenuItem, through='OrderItem')
status = models.CharField(max_length=50, choices=OrderStatus.choices, default=OrderStatus.NEW)
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
menu_item = models.ForeignKey(MenuItem, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField()
And the following serializers:
class MenuItemSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(max_length=40)
description = serializers.CharField(max_length=150)
price = serializers.FloatField(default=0.0)
category = serializers.CharField(max_length=50)
def create(self, validated_data):
return MenuItem.objects.create(**validated_data)
class OrderItemSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
menu_item_id = serializers.PrimaryKeyRelatedField(queryset=MenuItem.objects.all(), source='menu_item', read_only=False)
quantity = serializers.IntegerField(min_value=0)
class OrderSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
customer = serializers.CharField(max_length=50)
order_time = serializers.DateTimeField(read_only=True)
items = OrderItemSerializer(many=True)
def create(self, validated_data):
order_items_data = validated_data.pop('items')
order = Order.objects.create(**validated_data)
for order_item_data in order_items_data:
quantity = order_item_data.pop('quantity')
menu_item = order_item_data.pop('menu_item')
OrderItem.objects.create(order=order, quantity=quantity, menu_item=menu_item)
return order
And then in views:
@csrf_exempt
def order(request):
if request.method == 'POST':
data = JSONParser().parse(request)
serializer = OrderSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=201)
print(serializer.errors)
return JsonResponse(serializer.errors, status=400)
An example request looks like:
echo -n '{"customer": "John Doe", "items": [{"menu_item_id": 1, "quantity": 2}, {"menu_item_id": 2, "quantity": 1}]}' | http POST http://127.0.0.1:8000/order
When serializer.data is called an error is thrown:
AttributeError: Got AttributeError when attempting to get a value for field
menu_item_idon serializerOrderItemSerializer. The serializer field might be named incorrectly and not match any attribute or key on theMenuIteminstance. Original exception text was: 'MenuItem' object has no attribute 'menu_item'.
So its gone through the serializer.save() but errors on serializer.data. Can't work out why it can't get a value menu_item_id as it should be related to the id on MenuItem with the foreign key.
The issue here is that you're accidentally applying the
OrderItemSerializerto the wrong model. InOrderSerializertheitemsfield is defined as anOrderItemSerializer, but theitemsfield in the model refers to theMenuItemmodel. TheOrderItemmodel is only the through-model of the M2M relationship here.Here are the updated models and serializers that you need.
models.py
Notice that I added
related_name="order_items"to the FK field that relates it to theOrdermodel, so that we can reference this data model easily from theOrderSerializer.serializers.py
I changed the name of the field referencing
OrderItemSerializertoorder_itemsso that the serializer is working withOrderItemobjects now (using the related_name I just configured). The serializer now expects to receive anorder_itemsfield in the POST body, so make sure to reformat your requests. And in the first line of thecreatemethod I made sure we're poppingorder_itemsas well, instead ofitems.EDIT
If you want to take just the ID of the
MenuItemin the request body, but have the response include all theMenuItemdata, you can do this with aserializer.Field. I'll set it up so that the serializer can accept input data in a different format than it outputs.serializers.py
to_internal_valueuses the data provided (the object's ID) to find theMenuItemobject when the serializer is being used to save data. But when the serializer is being used to show data, theto_representationmethod is called, which uses theMenuItemSerializerto show all the object's data.Notice that I replaced the
menu_item_idline ofOrderItemSerializerwithmenu_item, which uses the newMenuItemFieldI defined. This means that your request body should usemenu_iteminstead ofmenu_item_id.