Reputation: 680
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:
Client
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?
name
field anyways?/api/Client/<id:client_id>/Invoice
Serializer
classes for each model - one for it's own viewset, and another for use in other models' viewsets?Thanks!
Upvotes: 1
Views: 1023
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