Home > Software engineering >  Serializers.validated_data fields got changed with source value in DRF
Serializers.validated_data fields got changed with source value in DRF

Time:09-24

I am trying to create an api, where user can create programs and add rules to them. Rules has to be executed in order of priority. I am using Django rest framework to achieve this and I am trying to achieve this using Serializer without using ModelSerializer. Provide your solution using serializers.Serializer class

One program can have many rules and one rule can be in many programs. So, I am using many_to_many relationship and also I want the user to change the order of rules in a program, to achieve this I am using a through table called Priority, where I keep track of relationship between a rule and a program using priority field

models.py

class Program(models.Model):
    name = models.CharField(max_length=32)
    description = models.TextField(blank=True)

    rules = models.ManyToManyField(Rule, through='Priority')

class Rule(models.Model):
    name = models.CharField(max_length=20)
    description = models.TextField(blank=True)
    rule = models.CharField(max_length=256)

class Priority(models.Model):
    program = models.ForeignKey(Program, on_delete=models.CASCADE)
    rule = models.ForeignKey(Rule, on_delete=models.CASCADE)

    priority = models.PositiveIntegerField()

    def save(self, *args, **kwargs):
        super(Priority, self).save(*args, **kwargs)

serializers.py

class RuleSerializer(serializers.Serializer):
    name = serializers.CharField(max_length=20)
    description = serializers.CharField(allow_blank=True)
    rule = serializers.CharField(max_length=256)

    def create(self, validated_data):
        return Rule.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.name = validated_data.get('name', instance.name)
        instance.description = validated_data.get('description', instance.description)
        instance.rule = validated_data.get('rule', instance.rule)
        instance.save()
        return instance

class PrioritySerializer(serializers.Serializer):
    rule_id = serializers.IntegerField(source='rule.id')
    rule_name = serializers.CharField(source='rule.name')
    rule_rule = serializers.CharField(source='rule.rule')
    priority = serializers.IntegerField()

class ProgramSerializer(serializers.Serializer):
    id = serializers.IntegerField()
    name = serializers.CharField(max_length=32)
    description = serializers.CharField(style={'base_template': 'textarea.html'})
    rules = PrioritySerializer(source='priority_set', many=True)

    def create(self, validated_data):
        rules_data = validated_data.pop('rules')
        program_obj = Program.objects.create(**validated_data)

        priority = 0
        for rule in rules_data:
            rule_obj = Rule.objects.get(pk=rule.rule_id)
            priority  = 1
            Priority.objects.create(program=program_obj, rule=rule_obj, priority=priority)

        return program_obj

views.py

class ProgramList(APIView):
    """
    List all programs, or create a new program.
    """
    def get(self, request, format=None):
        programs = Program.objects.all()
        serializers = ProgramSerializer(programs, many=True)
        return Response(serializers.data)

    def post(self, request, format=None):
        serializer = ProgramSerializer(data=request.data, partial=True)
        print(serializer.initial_data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

When I make a get request to http://localhost:8000/api/programs/, I got this response

[
    {
        "id": 1,
        "name": "some",
        "description": "hahah",
        "rules": [
            {
                "rule_id": 3,
                "rule_name": "DAIEA",
                "rule_rule": "date,and,invoice,equal,amount",
                "priority": 1
            },
            {
                "rule_id": 2,
                "rule_name": "DAIEA",
                "rule_rule": "date,and,invoice,equal,amount",
                "priority": 2
            },
            {
                "rule_id": 1,
                "rule_name": "DAI=A",
                "rule_rule": "date,and,invoice,equal,amount",
                "priority": 3
            }
        ]
    },
    {
        "id": 2,
        "name": "DAI=AS",
        "description": "Date and Invoice equal Amount Sumif",
        "rules": []
    },
]

I got list of rules under rules field, but when I make post request with this request.data to create a new program with rules with the help of rule.id,

Post data:

{
    "name": "DAI=AS",
    "description": "Date and Invoice equal Amount Sumif",
    "rules": [
        {
            "rule_id": 1
        },
        {
            "rule_id": 2
        }
    ]
}

After the serialization process, validated_data contains priority_set field instead of rules field, like below

{
    'name': 'DAI=AS', 
    'description': 'Date and Invoice equal Amount Sumif', 
    'priority_set': [
        OrderedDict([('rule', {'id': 1})]), 
        OrderedDict([('rule', {'id': 2})])
     ]
}

I don't want the serializer to change rules to priority_set

And also, I am getting list of OrderedDict in priority_set, instead I need dictionary of rule objects

This is what I want after serialization process,

{
    "name": "DAI=AS",
    "description": "Date and Invoice equal Amount Sumif",
    "rules": [
        {
            "rule_id": 1
        },
        {
            "rule_id": 2
        }
    ]
}

Thanks in advance

CodePudding user response:

Solution 1:

The fields in validated data is renamed with the source attribute. You can use the renamed attribute in create method of serializer.

For nested serializers, seems the validated data contains OrderedDict. You can convert it to regular Dict, and can get the rule id.

class ProgramSerializer(serializers.Serializer):
    id = serializers.IntegerField()
    name = serializers.CharField(max_length=32)
    description = serializers.CharField(style={'base_template': 'textarea.html'})
    rules = PrioritySerializer(source='priority_set', many=True)

    def create(self, validated_data):
        rules_data = validated_data.pop('priority_set')
        program_obj = Program.objects.create(**validated_data)

        priority = 0
        for rule in rules_data:
            rule_obj = Rule.objects.get(pk=dict(rule)["rule"]["id"])
            priority  = 1
            Priority.objects.create(program=program_obj, rule=rule_obj, priority=priority)

        return program_obj

Solution 2:

You can make rules with source name priority_set as read_only and can extract it from request data.

class ProgramSerializer(serializers.Serializer):
    id = serializers.IntegerField()
    name = serializers.CharField(max_length=32)
    description = serializers.CharField(style={'base_template': 'textarea.html'})
    rules = PrioritySerializer(source='priority_set', many=True, read_only=True)

    def create(self, validated_data):
        rules_data = self.context["request"].data["rules"]
        program_obj = Program.objects.create(**validated_data)

        priority = 0
        for rule in rules_data:
            rule_obj = Rule.objects.get(pk=rule["rule_id"])
            priority  = 1
            Priority.objects.create(program=program_obj, rule=rule_obj, priority=priority)

        return program_obj

You will need to pass request context to serializer

serializer = ProgramSerializer(data=request.data, partial=True, context={"request": request})

Solution 3:

Add related_name in program Foreign key in Priority model.

program = models.ForeignKey(Program, related_name="rule", on_delete=models.CASCADE)

Rename rules field in ProgramSerializer to something else e.g. rule, and remove source.

class ProgramSerializer(serializers.Serializer):
    id = serializers.IntegerField()
    name = serializers.CharField(max_length=32)
    description = serializers.CharField(style={'base_template': 'textarea.html'})
    rule = PrioritySerializer(many=True)

    def create(self, validated_data):
        rules_data = validated_data.pop('rule')
        program_obj = Program.objects.create(**validated_data)

        priority = 0
        for rule in rules_data:
            rule_obj = Rule.objects.get(pk=dict(rule)["rule"]["id"])
            priority  = 1
            Priority.objects.create(program=program_obj, rule=rule_obj, priority=priority)

        return program_obj

Now you will be able to post and retrieve data with rule key. You can rename it something else as appropriate.

  • Related