How to design ArrayField in django rest framework?

Viewed 428

I am making API for cook book with Django Rest Framework. I don't know how to design ingredients model to make data to be like this:

{
        "id": 1,
        "name": "spaghetti",
        "recipe": "recipe",
        "ingredients": [
            [{name:'pasta',amount:100},{name:'tomato',amount:200},{...}]
        ],    
    }

My model:

class Meal(models.Model):
    name = models.TextField()    
    recipe = models.TextField()
    ingredients = ?

Also how to serialize this field?

4 Answers

You can create a separate model for ingredient.

Many to many relation will be the best for me, because of one meal can have many ingredients and in the opposite way one ingredient can be used to make many meals.

According to django docs, in your case:

models.py

from django.db import models

class Ingredient(models.Model):
    name = models.CharField(max_length=90)
    amount = models.FloatField()

    class Meta:
        ordering = ['name']

    def __str__(self):
        return self.name

class Meal(models.Model):
    name = models.CharField(max_length=100)
    recipe = models.CharField(max_length=100)
    ingredients = models.ManyToManyField(Ingredient)

    class Meta:
        ordering = ['name']

    def __str__(self):
        return self.name

serializers.py

class IngredientSerializer(serializers.ModelSerializer):
    class Meta:
        model = Ingredient
        fields = '__all__'


class MealSerializer(serializers.ModelSerializer):
    ingredients = IngredientSerializer(read_only=True, many=True)

    class Meta:
        model = Meal
        fields = '__all__'

I believe what you are looking for is JsonBField

from django.contrib.postgres.fields.jsonb import JSONField as JSONBField
ingredients = JSONBField(default=list,null=True,blank=True)

this should do what you expect, have a nice day

edit: thanks for the update as @Çağatay Barın mentioned below FYI, it is deprecated, Use django.db.models.JSONField instead.see the Doc

from , Django comes with JSONField

class Meal(models.Model):
    name = models.TextField()
    recipe = models.TextField()
    ingredients = models.JSONField()

There are two approaches to have this outcome [2] having another model:

  1. using WritableNestedModelSerializer.
  2. Overwriting the create() method of your serializer.

1st example, using WritableNestedModelSerializer:

# models.py --------------------------

class Ingredient(models.Model):
    # model that will be related to Meal.
    name = models.CharField(max_lenght=128)
    amount = models.IntergerField()
    
    def __str__(self):
        return str(self.name)

class Meal(models.Model):
    # meal model related to Ingredient.
    name = models.TextField()    
    recipe = models.TextField()
    ingredients = models.ForeigKey(Ingredient)

    def __str__(self):
        return str(self.name)

# serializers.py ----------------------

class IngredientSerializer(serializers.ModelSerializer):
    
    class Meta:
        model = Ingredient
        fields = '__all__'

class MealSerializer(WritableNestedModelSerializer, serializers.ModelSerializer):

    ingredients_set = IngredientSerializer(required=False, many=True)
    
    class Meta:
        model = Ingredient
        fields = ["id", "name","recipe", "ingredients_set"]

2nd example rewriting the create() method:

# models.py --------------------------

# Follow the same approach as the first example....

# serializers.py ----------------------

class IngredientSerializer(serializers.ModelSerializer):
    
    class Meta:
        model = Ingredient
        fields = '__all__'

class MealSerializer(serializers.ModelSerializer):

    ingredients = IngredientSerializer(required=False, many=True)
    
    class Meta:
        model = Meal
        fields = ["id", "name","recipe", "ingredients"]

    def create(self, validated_data):
        # 1st step.
        ingredients = validated_data('ingredients')
        # 2nd step.
        actual_instance = Meal.objects.create(**validated_data)
        # 3rd step.
        for ingredient in ingredients:
            ing_objects = Ingredients.object.create(**ingredient)
            actual_instance.ingredients.add(ing_objects.id)
        actua_instance.save()
   
        return actual_instance

What was done in the second example?

1st step: since you create a one-2-many relationship the endpoint will wait for a payload like this:

    {

        "name": null,
        "recipe": null,
        "ingredients": [],    
    }

ex of validated_data/ the data you sent:

    {
        "id": 1,
        "name": "spaghetti",
        "recipe": "recipe",
        "ingredients": [
            {name:'pasta',amount:100},{name:'tomato',amount:200},{...}
        ],    
    }

Therefore, since you are sending a payload with many ingredientes inside the ingredient array you will get this value from the validated_data.

For instance, if you make a print of the 'ingredients'(from the inside of the create() method) this is what you will get in your terminal:

[{name:'pasta',amount:100},{name:'tomato',amount:200},{...}]

2nd step: Alright, since you get the ingredients from validate_data it is time to create a Meal instance (where will be without the 'ingredients').

3rd step: You will loop all the ingredients objects from the 1st step you have done above and add them into the Meal.ingredients relationship saving the Meal instance.

-- about the extra model --

[2] Bear in mind that having a JSONField() allows anything to be added there even extra fields. Having a Meal model might be a better option if you want have a better control.

Related