Aman
Aman

Reputation: 680

Creating a Serializer to work with model relationships

I am new with Django Rest Framework and wanted to understand what's the accepted practice for writing Serializers that work with nested relationships.

Say, I have a models called Client and Invoice (this is just an illustrative example):

class Client(models.Model)
    name = models.CharField(max_length=256)    


class Invoice(models.Model)
    client = models.ForeignKey(Client)
    date = models.DateTimeField()
    amount = models.DecimalField(max_digits=10, decimal_places=3)

I want to create a Serializer for Client that supports the following use cases:

  1. Create a Client
  2. When I create an Invoice, refer to the Client using its id.

Let's say I use this implementation:

class ClientSerializer(serializers.ModelSerializer):
    class Meta:
        model = Client
        fields = ['id', 'name']


class InvoiceSerializer(serializers.ModelSerializer):
    client = ClientSerializer()

    class Meta:
        model = Invoice
        fields = ['id', 'client', 'date', 'amount']

    def create(self, data):
        client = Client.objects.get(pk=data['client']['id'])
        invoice = Invoice(client=client, 
                          date=datetime.fromisoformat(data['date']),
                          amount=Decimal(data['amount']))
        invoice.save()

With this code, if I try to create an Invoice, I need the client object in the POST data to contain name as well. There is no config of the name field (read_only=True, write_only=True, required=False) that allows me to create and read Client as well as not be required when creating the Invoice.

How should this be solved?

Thanks!

Upvotes: 1

Views: 1023

Answers (1)

Ozgur Akcali
Ozgur Akcali

Reputation: 5492

This is an accepted pratice, but it has its advantages and disadvantages. Actual good practice depends on your actual needs. Here, as you suggested, while creating an Invoice, you also need to send a client name in the request, which should not be necessary. To overcome that need, one possible practive can be as follows:

class ClientSerializer(serializers.ModelSerializer):
    class Meta:
        model = Client
        fields = ['id', 'name']


class InvoiceSerializer(serializers.ModelSerializer):
    client = serializers.PrimaryKeyRelatedField(queryset=Client.objects.all())

    class Meta:
        model = Invoice
        fields = ['id', 'client', 'date', 'amount']

With this approach, you only include client's id in the serializer. You'll only need to send a client id in the requset with this approach, and don't need to write a custom create method on the serializer. Disadvantage of this approach is; you do not have the client name when listing invoices. so if you need to display client name when displaying an invoice, we'd need to improve this solution a bit:

class InvoiceSerializer(serializers.ModelSerializer):
    client = serializers.PrimaryKeyRelatedField(queryset=Client.objects.all())
    client_details = ClientSerializer(source='client', read_only=True)

    class Meta:
        model = Invoice
        fields = ['id', 'client', 'client_details', 'date', 'amount']

With this approach, we have added a read-only field, client_details, that keeps the data in client serilaizer. So for write operations we use client field, which is only an id, and to read details about a client, we use client_details field.

Another approach could be defining a separate client serializer to be used as a child serializer in InvoiceSerializer only:

class ClientSerializer(serializers.ModelSerializer):
    class Meta:
        model = Client
        fields = ['id', 'name']


class InvoiceClientSerializer(serializers.ModelSerializer):
    name = serializers.CharField(read_only=True)

    class Meta:
        model = Client
        fields = ['id', 'name']

class InvoiceSerializer(serializers.ModelSerializer):
    client = InvoiceClientSerializer()

    class Meta:
        model = Invoice
        fields = ['id', 'client', 'date', 'amount']

    def create(self, data):
        client = Client.objects.get(pk=data['client']['id'])
        invoice = Invoice(client=client, 
                          date=datetime.fromisoformat(data['date']),
                          amount=Decimal(data['amount']))
        invoice.save()

In this approach, we have defined a specia client serializer for use in InvoiceSerializer only, that has name field as read only. So while creating / updating an Invoice, you won't need to send client name, but while listing invoices, you get the client name. Advantage of this approach to the one before use, we do not need to use two separate fields for client field for writing and reading details.

For your second question, it is not supported out of the box by DRF, but you can take a look at this package, which provides that functionality and is listed on DRF's own documentation: https://github.com/alanjds/drf-nested-routers

Upvotes: 2

Related Questions