Search code examples
pythondjangodjango-rest-frameworkgeneric-relations

Django serialize child model from inheritance field


I'm using DRF and I have what appears to be a design issue with my models:

Background: I'm doing a personal project for finance administration, so I have models for SavingsAccount, CreditCard, Incomes (this is really simplified but I think that is enough to see the problem).

The problem: In incomes I should be able to track to which account I added the money, but that could be to a savings account or to a credit card, so I made another model with the common attributes for both called Account.

I'm using InheritanceManager to make it a little bit easier.

from model_utils.managers import InheritanceManager

class Account(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=120)
    balance = models.DecimalField(decimal_places=4, max_digits=12)
    objects = InheritanceManager()

    def get_child(self):
        return Account.objects.get_subclass(pk=self.pk)

class SavingsAccount(Account):
    bank = models.CharField(max_length=120)
    is_investment = models.BooleanField(default=0)


class CreditCard(Account):
    cut = models.IntegerField()
    pay = models.IntegerField()
    bank = models.CharField(max_length=120)
    credit = models.DecimalField(decimal_places=4, max_digits=12)

    @property
    def used(self):
        return self.credit - self.balance

class Income(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    amount = models.DecimalField(decimal_places=4, max_digits=12)
    account = models.ForeignKey(Account, on_delete=models.PROTECT, related_name="incomes")
    description = models.TextField()
    date = models.DateField()

with this I'm able to interact with the account with things like: income = Income.objects.select_related("account").first() and then income.account.get_child() to retrieve the SavingsAccount or CreditCard object, the problem is with the serializer, because I want to send the child object instead of the Account object.

My current (incomplete) solution: using rest-framework-generic-relations I got

class IncomeSerializer(serializers.ModelSerializer):
    account = GenericRelatedField({
        CreditCard: CreditCardSerializers(),
        SavingsAccount: SavingsAccountSerializers(),
    })
    class Meta:
        Model = Income
        fields = ("id", "amount", "account", "description")

This fails because the account object that the serializer gets is the Account type, how can I get an Income QuerySet with the account child classes instead of the default one?

Is this a bad idea? how should I implement something like this? it would be better to just send the id of the account object and then do another request for that object?

Thanks a lot in advance, I tried to put all the needed information but please let me know if I should add more.

I've been avoiding to use the ContentType, because I don't consider it needed here.


Solution

  • Well, for now I did it like this:

    class AccountSerializer(serializers.ModelSerializer):
        def to_representation(self, value):
            child = value.get_child()
            if isinstance(child, SavingsAccount):
                serializer = SavingsAccountSerializer(child)
            elif isinstance(child, Wallet):
                serializer = WalletSerializer(child)
            else:
                raise Exception('Unexpected type of tagged object')
    
            return serializer.data
    
        class Meta:
            model = Account
    
    class WalletSerializer(serializers.ModelSerializer):
        class Meta:
            model = Wallet
            fields = ('__all__')
    
    
    class SavingsAccountSerializer(serializers.ModelSerializer):
        class Meta:
            model = SavingsAccount
            fields = ('__all__')
    
    
    class IncomeSerializer(serializers.ModelSerializer):
        account = AccountSerializer(read_only=True)
        class Meta:
            model = Income
            fields = ("__all__")
    

    I'm sure that this is not the most efficient way due to the connections used for the get_child method in each object, that uses n number of queries (where n is the number of objects in incomes), I'm pretty sure that you can do it with 2 queries (one for each child class) but I'm just learning, hopefully someone more experienced will post a better answer.

    Even though, I let this in case that helps someone working in a small project just like me.

    Note: I'm just using it to retrieve information, I haven't test it for retrieving information.