Search code examples
pythondjangodjango-rest-frameworkdjango-serializer

How to serialize each foreign key object with different serializer class in django rest framework


So I'm wondering if it is possible to serialize each foreign key object with different serializer in django rest framework.

What I mean is:

I have my models like

class KingdomModel(models.Model):
    kingdom_name = models.CharField(max_length=32)
    owner = models.OneToOneField(User, on_delete=models.CASCADE)
    faction = models.CharField(max_length=10)
    

class CityModel(models.Model):
    kingdom = models.ForeignKey(KingdomModel, on_delete=models.CASCADE, related_name="cities")
    city_name = models.CharField(max_length=32)
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    """
    ... other fields aswell
    """


class ArmyModel(models.Model):
    home_city = models.ForeignKey(CityModel, on_delete=models.CASCADE, null=True, related_name="own_troops")
    current_city = models.ForeignKey(CityModel, on_delete=models.CASCADE, null=True, related_name="all_troops", blank=True)
    status = models.CharField(max_length=32)
    action_done_time = models.DateTimeField(default=None, null=True, blank=True)
    target_city = models.ForeignKey(CityModel, on_delete=models.CASCADE, null=True, related_name="incoming_troops", default=None, blank=True)

    # Shared troops
    settlers = models.IntegerField(default=0)

    # Gaul troops
    pikemen = models.IntegerField(default=0)
    swordmen = models.IntegerField(default=0)
    riders = models.IntegerField(default=0)

    # Roman troops
    legionaries = models.IntegerField(default=0)
    praetorian = models.IntegerField(default=0)


And I am trying to serialize the armies based on the kingdoms faction. Which works fine when talking about own_troops because they are always going to be serialized with the same serializer, like so.

class CitySerializer(serializers.ModelSerializer):

    own_troops = serializers.SerializerMethodField()
    incoming_troops = serializers.SerializerMethodField()

    def get_own_troops(self, city_obj):
        if(KingdomModel.objects.get(owner=city_obj.owner).faction == "Gaul"):
            return GaulTroopsSerializer(instance=city_obj.own_troops, context=self.context, many=True, required=False, read_only=False).data
        elif(KingdomModel.objects.get(owner=city_obj.owner).faction == "Roman"):
            return RomanTroopsSerializer(instance=city_obj.own_troops, context=self.context, many=True, required=False, read_only=False).data
class RomanTroopsSerializer(serializers.ModelSerializer):
    class Meta:
        model = ArmyModel
        fields = ['id', 'home_city', 'current_city', 'target_city', 'status', 'action_done_time', 'settlers', 'legionaries', 'praetorian']

class GaulTroopsSerializer(serializers.ModelSerializer):
    class Meta:
        model = ArmyModel
        fields = ['id', 'home_city', 'current_city', 'target_city', 'status', 'action_done_time', 'settlers', 'pikemen', 'swordmen', 'riders']

But if I try to apply the same logic to serializing the incoming_troops, it will always serialize all of the objects in the list with the first serializer. This was my hopeless attempt at serializing each foreign key with different serializer based on the data inside the relation.


    def get_incoming_troops(self, city_obj):
        for data in GaulTroopsSerializer(instance=city_obj.incoming_troops, context=self.context, many=True, required=False, read_only=False).data:
            print(data)
            home_city_obj = CityModel.objects.get(id=data['home_city'])
            if(KingdomModel.objects.get(owner=home_city_obj.owner).faction == "Gaul"):
                return GaulTroopsSerializer(instance=city_obj.incoming_troops, context=self.context, many=True, required=False, read_only=False).data
            else:
                return RomanTroopsSerializer(instance=city_obj.incoming_troops, context=self.context, many=True, required=False, read_only=False).data

    class Meta:
        model = CityModel
        fields = ['id', 'owner', 'city_name', 'x_coordinate', 'y_coordinate', 'last_updated', 'max_warehouse_capacity', 'max_grain_silo_capacity', 'wood_ammount', 'wheat_ammount', 'stone_ammount', 'iron_ammount', 'resource_fields', 'buildings','incoming_troops', 'own_troops',  'all_troops']
        read_only_fields = ['id', 'max_warehouse_capacity', 'max_grain_silo_capacity']

I know I could just have multiple models for all of the different factions armies, but for now I am just wondering if this is possible in django / drf?


Solution

  • Answering my own question because I got it working and what I did is the following:

    First of all I scraped the multiple troop serializers. And had just one army serializer where I switch the fields according to the faction.

    This is my ArmySerializer now

    class ArmySerializer(serializers.ModelSerializer):
    class Meta:
        model = ArmyModel
        fields = ['id', 'home_city', 'current_city', 'target_city', 'status', 'action_done_time']  
        
    
    def to_representation(self, instance):
        try:
            del self.fields # Clear the cache
        except AttributeError:
            pass
    
        if("faction" in self.context and len(self.context['faction']) > 0):
            print(self.context['faction'])
            self.fields['settlers'] = serializers.IntegerField()
            if(self.context['faction'][0] == "Gaul"):
                self.fields['pikemen'] = serializers.IntegerField()
                self.fields['swordmen'] = serializers.IntegerField()
                self.fields['riders'] = serializers.IntegerField()
            elif(self.context['faction'][0] == "Roman"):
                self.fields['legionaries'] = serializers.IntegerField()
                self.fields['praetorian'] = serializers.IntegerField()
            if(len(self.context['faction']) > 1):
                self.context['faction'].pop(0)
        
        
        return super().to_representation(instance)
    

    And in CitySerializer I am populating the context['faction'] list like this:

    class CitySerializer(serializers.ModelSerializer):
    
    own_troops = serializers.SerializerMethodField()
    incoming_troops = serializers.SerializerMethodField()
    
    
    def get_own_troops(self, instance):
        if(KingdomModel.objects.get(owner=instance.owner).faction == "Gaul"):
            self.context["faction"] = ["Gaul"]
            return ArmySerializer(instance=instance.own_troops, context=self.context, many=True, required=False, read_only=False).data
        elif(KingdomModel.objects.get(owner=instance.owner).faction == "Roman"):
            self.context["faction"] = ["Roman"]
            return ArmySerializer(instance=instance.own_troops, context=self.context, many=True, required=False, read_only=False).data
    
    def get_incoming_troops(self, city_obj):
        self.context['faction'] = []
        for data in ArmySerializer(instance=city_obj.incoming_troops, context=self.context, many=True, required=False, read_only=False).data:
            home_city = CityModel.objects.get(id=data['home_city'])
            sender_faction = KingdomModel.objects.get(owner=home_city.owner).faction
            if(sender_faction == "Gaul"):
                self.context['faction'] += ["Gaul"]
            else:
                self.context['faction'] += ["Roman"]
        return ArmySerializer(instance=city_obj.incoming_troops, context=self.context, many=True, required=False, read_only=False).data
    

    Also it should be said that, this introduced a new problem when creating an army with POST requests. The fields that are dynamically added in the to_representation method are not validated by default, so they are not present in validated_data. There might be a way to override validation but I just took them from the raw request data for now and it seems to be working fine.