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:
Create a Client
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?
Is the accepted practice that the request include the name field anyways?
Can we somehow created nested models like this? /api/Client/<id:client_id>/Invoice
Do we create multiple Serializer classes for each model - one for it's own viewset, and another for use in other models' viewsets?
Thanks!
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
Related
I have an example model which has a fk relation with user model and Blog model. Now I have a get api which only requires certain fields of user to be displayed.
My model:
class Example(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=True,
related_name="user_examples",
)
blog = models.ForeignKey(
Blog,
on_delete=models.CASCADE,
null=True,
related_name="blog_examples",
)
/................./
Now my view:
class ExampleView(viewsets.ModelViewSet):
queryset = Example.objects.all()
serializer_class = ExampleSerializer
def list(self, request, *args, **kwargs):
id = self.kwargs.get('pk')
queryset = Example.objects.filter(blog=id)
serializer = self.serializer_class(queryset,many=True)
return Response(serializer.data,status=200)
My serializer:
class ExampleSerializer(serializers.ModelSerializer):
class Meta:
model = Example
fields = ['user','blog','status']
depth = 1
Now when I call with this get api, I get all example objects that is required but all the unnecessary fields of user like password, group etc . What I want is only user's email and full name. Same goes with blog, I only want certain fields not all of them. Now how to achieve this in a best way??
You will have to specify the required fields in nested serializers. e.g.
class BlogSerializer(serializers.ModelSerializer):
class Meta:
model = Blog
fields = ['title', 'author']
class ExampleSerializer(serializers.ModelSerializer):
blog = BlogSerializer()
class Meta:
model = Example
fields = ['user','blog','status']
are you setting depth in serializer's init method or anywhere else? beacause ideally it should only display id's and not anything else. if yes then set depth to zero and use serializer's method field to return data that you need on frontend. I can provide you with example code samples
This is just my curiosity but I will be very happy if anyone answers my question.
I am using Django Rest Framework but I'm a beginner. In serializers.py, I use ModelSerializer and "all" to fields attribute.
This is an example.
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
And then, I just thought
when don't we use "__all__" in serializers.py??
As long as we create models.py in advance, I think we usually use all fields in each Model.
I would like you to teach me when we omit specific fields that come from each Model.
Thank you.
So the second question is a bit harder to explain in a comment:
If we use some fields of all fields in Model, how do we store information of the rest of fields?
Various cases:
Fields with defaults:
class Log(models.Model):
message = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class LogSerializer(serializers.ModelSerializer):
class Meta:
model = Log
fields = ('message',)
For autogenerated, think user profile models via the post_save signal or calculated fields:
class OrderLine(models.Model):
order = models.ForeignKey(Order)
name = models.CharField(max_length=200)
quantity = models.IntegerField()
price = models.DecimalField()
class OrderLineSerializer(serializers.ModelSerializer):
order = serializers.PrimaryKeyRelatedField()
product = serializers.IntegerField()
class Meta:
model = OrderLine
fields = ('quantity', 'product', 'order')
In this case, the product is a primary key for a product. The serializer will have a save method that looks up the product and put it's name and price on the OrderLine. This is standard practice as you cannot reference a product in your orders, else your orders would change if you change (the price of) your product.
And derived from request:
class BlogPost(models.Model):
author = models.ForeignKey(User)
post = models.TextField()
class BlogPostSerializer(serializers.ModelSerializer):
class Meta:
model = BlogPost
fields = ('post',)
def create(self, validated_data):
instance = BlogPost(**validated_data)
instance.author = self.context['request'].user
instance.save()
return instance
This is pretty much the common cases.
There are many cases, but I think the two main ones are:
When you don't want all fields to be returned by the serializer.
When you need some method of the serializer to know its fields. In such case, you should traverse fields array, but it doesn't work if you use __all__, only if you have an actual list of fields.
Model:
class Demo(models.Model):
name = models.CharField(max_length=255)
desc = models.TextField()
user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
Serializer:
class DemoSerializer(serializers.ModelSerializer):
class Meta:
model = Demo
fields = '__all__'
I have form in frontend side where I'm adding name, desc and assigning to User so here I'm getting on an issue.
I'm passing data to API {name: "demo", desc: "lorem ipsum", user: 1 }
It's working on save but after saving it's return same response but I want user first_name, last_name, and email in return response.
Because I have a table showing a list of demo table content. but always getting only User ID not a detail of user.
If I'm increasing depth of Serializer It's creating an issue in save time but on get records time I'm getting all details of User model. Like Password also in response so that is a security issue for me show all thing.
You can use depth = 1 to get all the data of foreign key object:
class DemoSerializer(serializers.ModelSerializer):
class Meta:
model = Demo
fields = '__all__'
depth = 1
You could separate the Create and Retrieve serializer. For example, the create serializer will be the one you are currently using:
class DemoCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Demo
fields = '__all__'
On the other hand, Retrieve serializer will serialize the User with a different serializer using Nested Relationship.
class DemoRetrieveSerializer(serializers.ModelSerializer):
user = UserMinimalSerializer # or you could use your UserSerializer, see the last part of the answer
class Meta:
model = Demo
fields = ('id', 'name', 'desc', 'user')
read_only = ('id', 'name', 'desc', 'user',)
In your view, you will create the data with the first serializer and respond with the second. An example using APIView:
class DemoView(APIView):
def post(self, request, format=None):
create_serializer = DemoCreateSerializer(data=request.data)
if create_serializer.is_valid():
instance = create_serializer.save()
retrive_serializer = DemoRetrieveSerializer(instance)
return Response(retrive_serializer.data, status=status.HTTP_201_CREATED)
return Response(create_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
You might have to customize DRF provided views to achieve this, i.e. for Generic views.
Since you don't want to include all the fields of User model, you will have to write a minimal representation of User using another serializer.
class UserMinimalSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'first_name', 'last_name', 'email')
read_only = ('id', 'first_name', 'last_name', 'email',)
Hope it helps.
I feel like this is a super basic question but am having trouble finding the answer in the DRF docs.
Let's say I have a models.py set up like so:
#models.py
class Person(models.Model):
name = models.CharField(max_length=20)
address = models.CharField(max_length=20)
class House(models.Model):
name = models.CharField(max_length=20)
owner = models.ForeignKey(Person)
And I have a ModelSerializer set up like so:
#serializers.py
class House(serializers.ModelSerializer):
class Meta:
model = House
fields = '__all__'
What I want to do is to be able to POST new House objects but instead of having to supply the pk of the Person object, I want to be able to supply the name of the Person object.
E.g.
post = {'name': 'Blue House', 'owner': 'Timothy'}
The actual models I'm using have several ForeignKey fields so I want to know the most canonical way of doing this.
One solution may be to use a SlugRelatedField
#serializers.py
class House(serializers.ModelSerializer):
owner = serializers.SlugRelatedField(
slug_field="name", queryset=Person.objects.all(),
)
class Meta:
model = House
fields = '__all__'
This will also change the representation of your serializer though, so it will display the Person's name when you render it. If you need to render the Person's primary key then you could either override the House serializers to_representation() method, or you could implement a small custom serializer field by inheriting SlugRelatedField and overriding to_representation() on that instead.
Change your serializer as below by overriding the create() method
class House(serializers.ModelSerializer):
owner = serializers.CharField()
class Meta:
model = House
fields = '__all__'
def create(self, validated_data):
owner = validated_data['owner']
person_instance = Person.objects.get(owner=owner)
return House.objects.create(owner=person_instance, **validated_data)
I have written basic model serializers in Django where the api mimics the data model. I now have a requirement to store User Preference in database. The api contains an array.
My User Models :
class User(models.Model):
email_id = models.EmailField(max_length=80, blank=True, primary_key=True)
class UserPreference(models.Model)
email_id = models.ForeignKey('User')
preference = models.CharField(maxlength=20)
An ideal json post request would look something like this
{
email:"abhishek#gmail.com"
preference : [ 'books', 'food', 'lifestyle', 'travel']
}
I wish to save this json schema to the UserPreference model. This requires multiple inserts for preference. What will be a good serializer design for it ?
I tried
class UserPreferenceSerializer(serializers.ModelSerializer):
class Meta:
model = UserPreference
fields = ('email_id', 'preference')
you could use StringRelatedField of Django Rest Framework.
Make below changes and you will get the response in the way you want.
models.py (put related_name there)
class UserPreference(models.Model):
email_id = models.ForeignKey(User, related_name='preference')
preference = models.CharField(maxlength=20)
serializers.py
class UserSerializer(serializers.ModelSerializer):
preference = serializers.StringRelatedField(many=True)
class Meta:
model = User
fields = ('email_id', 'preference')
You could make your model like this:
class UserPreference(models.Model)
email_id = models.ForeignKey('User')
preference = models.ManyToManyField('Preference') #create model for preferences
Then add custom create method to your serializer:
def create(self, validated_data):
user = self.context.get('user') #you can pass context={'user': self.request.user} in your view to the serializer
up = UserPreference.objects.create(email_id=user)
up.save()
preference = validated_data.get('preference', [])
up.preference.add(*preference)
up.save()
return up