DRF Serialize 2 fields into array - django-rest-framework

DRF v3.12.4
class AModel(models.Model):
field_0 = models.CharField()
field_1 = models.FloatField()
field_2 = models.FloatField()
Have such a model, and I need to serialize it (read-only) in such way:
{ "field_0": "FIELD0", "arr": [ 1.0, 2.0 ]}
For now, I make the next solution:
class BSerializer(serializer.Serializer):
def to_representation(self, data):
return [data.field_1, data.field_2]
class ASerilizer(serializers.Serializer):
arr = BSerializer(source='*')
class Meta:
model = AModel
fields = ['field_0', 'arr']
It's work, but I also use drf-spectacular and of course, it can't analyze the overriding method. I'm trying #extend_schema_serializer on BSerilizer, but it does not work.
Is it possible without overriding the to_representation method?
So, the question is it possible for such a case to use a serializer without overriding the to_representation method, ListSerializer for example? Or maybe someone knows why #extend_schema_serializer does not work?
Thx

If you want just use arr field as a read_only field, you can use serializer's SerializerMethodField:-
from rest_framework import serializers
class ASerilizer(serializers.Serializer):
arr = serializers.SerializerMethodField()
class Meta:
model = AModel
fields = ['field_0', 'arr']
def get_arr(self, instance):
return [instance.field_1, instance.field_2]
For more information you can refer from here https://www.django-rest-framework.org/api-guide/fields/#serializermethodfield

Related

Custome the display's field in django REST

I use Django REST and I would know if it is possible to customise the display of attributes in the json response.
Exemple :
class MyModel(models.Model):
name = models.CharField(max_length=300)
and my serializer :
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = ['name']
But instead to see {'name' : 'its value'}, I would see {'My customed model name' : 'its value'}.
Do you think that it's possible?
Thank you very much.
You can override the to_representation method of the serializer to change the name of the field:
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = ['name']
def to_representation(self, instance):
ret = super().to_representation(instance)
# ret is an OrderedDict, so this will change the order of the result.
ret['custom_name'] = ret.pop('name')
return ret
def to_internal_value(self, data):
# if you want to write to the serializer using your custom name.
data['name'] = data.pop('custom_name')
return super().to_internal_value(data)
One way you could do it is using a SerializerMethodField (https://www.django-rest-framework.org/api-guide/fields/#serializermethodfield)
class MyModelSerializer(serializers.ModelSerializer):
my_customed_model_name = serializers.SerializerMethodField()
def get_my_customed_model_name(self, obj):
return obj.name
class Meta:
model = MyModel
Although if you want the field name to have spaces in it, this solution won't work for you.
You can do this in this way
class MyModelSerializer(serializers.ModelSerializer):
other_name = serializers.CharField(source='name')
class Meta:
model = MyModel
fields = ['other_name']

How can i add #property field to django rest serializer with '__all__'

I have model
class A(models.Model):
is_enable = models.BooleanField(default=False)
title = models.CharField(max_length=255, blank=True, null=True)
updated_date = models.DateTimeField(auto_now=True)
show_count = models.IntegerField(default=0)
answers_count = models.IntegerField(default=0)
audience = JSONField()
events = JSONField()
rules = JSONField()
message = models.TextField(blank=True)
#property
def conversion(self):
if self.show_count == 0:
return 0.0
return (self.answers_count / self.show_count) * 100
And i have serializer
class ASerializer(serializers.ModelSerializer):
audience = serializers.JSONField()
events = serializers.JSONField()
rules = serializers.JSONField()
class Meta:
model = Trigger
fields = '__all__'
I want to add to response #property conversion` field
and I want to do something like this in serializer
class Meta:
model = Trigger
fields = '__all__' + conversion
I know that i can make something like this
class Meta:
model = Trigger
fields = ('is_enable', 'title' ... 'conversion')
But i want add all fields and conversion field and do this more beautiful
you can use somethig like this:
class ModelMixin:
#classmethod
def _get_model_fields(cls):
all_fields = cls._meta.get_fields()
fields = [i.name for i in all_fields if i.__class__.__name__ not in ['ManyToManyRel', 'GenericRelation', 'ManyToOneRel']]
return fields
#classmethod
def get_serializer_fields(cls):
fields = cls._get_model_fields()
return fields
class A(ModelMixin, models.Model):
...
class ASerializer(ModelSerializer):
custom_field = serializers.JSONField()
class Meta:
model = A
fields = A.get_serializer_fields()
fields.append('cusom_field')
Might still not be as clean as you want it to be, but using <model>._meta.fields you can get all fields of that model, and using the name property of each field you can retrieve the names that refer to them.
As the serializers file is interpreted as python, it supports code execution, meaning you can use all python functionalities when setting properties, including a for loop, which gives us the ability to do this:
fields = (tuple((f.name for f in A._meta.fields)) + ('conversion',))
Needed this for my own model as well and there it seemed to work, so hopefully so does it for you.
You can create a SerializerMethodField in your serializer and later define a method in serializer itself that can call your functions you defined in models using #property
class ASerializer(serializers.ModelSerializer):
audience = serializers.JSONField()
events = serializers.JSONField()
rules = serializers.JSONField()
conversion = serializers.SerializerMethodField()
class Meta:
model = Trigger
fields = '__all__'
def get_conversion(self,obj):
return obj.conversion
This should allow you to keep your code beautiful without modifying much of your code.

How to use DRF serializers to use multiple serializers and also flatten the data sent

I am struggling to figure out the best way to write these serializers.
I have a model like this
class Foo(models.Model):
db = models.CharField(max_length=30)
name = models.CharField(max_length=30)
schema = models.CharField(max_length=50)
table_name = models.CharField(max_length=50)
ext_type = models.CharField(max_length=100)
sample_by = models.CharField()
sample_by_val = models.CharField()
Next I have my serializer with something like this
class SaveSerializer(serializers.ModelSerializer): #
"""
Cannot figure out the best way to do this.
This is just an example serializer I had in mind.
I want to understand a cleaner/best way to do this.
"""
sample = SampleSerializer()
exts = ExtSerializer()
class Meta:
model = Foo
Here is the problem - The data to this view is sent in this format
{
"sample": {"sample_by":"weight", "sample_by_val":[30,50,60],"name":"test"}
"exts": {"ext_type": "BY WEIGHT", "table":"EMPLOYEE"},
"db" : "foobar",
"schema" : "abcd"
}
class ExtSerializer(serializers.Serializer):
table = serializers.CharField()
ext_type = serializers.CharField()
class SampleSerializer(serializers.Serializer):
sample_by = serializers.CharField()
# Similar ones for sample_by_val and name
I am struggling to write SaveSerializer. It needs to first check the data that is being sent in a particular format and pass it through some serializers. Then it needs to flatten out that data format to store it in the model itself.
Can someone help me understand the best way to do this? It will be very helpful! thanks
You can override the to_representation() and to_internal_value(), I think it's best way.
Here is an example
Sorry, I'll eat my word, just override create method like this:
#override create method of serializer, you can easily handle object creation.
def create(self, validated_data):
#first, pop sample and exts from validated data.
sample = validated_data.pop('sample')
exts = validated_data.pop('exts')
#then, create object
foo = Foo.objects.create(**validated_data, **sample, **exts) #flat validated_data, sample, and exts to create Foo.
return foo
#maybe you also want to handle representation, since the model of Foo doesn't has ext and sample fields
def to_representation(self, instance):
sample = SampleSerializer(instance).data #SampleSerializer will deserialize sample_by field of Foo instance.
exts = ExtSerializer(instance).data
instance.sample = sample
instance.exts = exts
return super(SaveSerializer, self).to_representation(instance)

How to dynamically remove fields from serializer output

I'm developing an API with Django Rest framework, and I would like to dynamically remove the fields from a serializer. The problem is that I need to remove them depending on the value of another field. How could I do that?
I have a serializer like:
class DynamicSerliazer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
url = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
elements = serializers.SerializerMethodField()
def __init__(self, *args, **kwargs):
super(DynamicSerliazer, self).__init__(*args, **kwargs)
if self.fields and is_mobile_platform(self.context.get('request', None)) and "url" in self.fields:
self.fields.pop("url")
As you can see, I'm already removing the field "url" depending whether the request has been done from a mobile platform. But, I would like to remove the "elements" field depending on the "type" value. How should I do that?
Thanks in advance
You can customize the serialization behavior by overriding the to_representation() method in your serializer.
class DynamicSerliazer(serializers.ModelSerializer):
def to_representation(self, obj):
# get the original representation
ret = super(DynamicSerializer, self).to_representation(obj)
# remove 'url' field if mobile request
if is_mobile_platform(self.context.get('request', None)):
ret.pop('url')
# here write the logic to check whether `elements` field is to be removed
# pop 'elements' from 'ret' if condition is True
# return the modified representation
return ret
You can create multiple serializers and choose the proper one in view
class IndexView(APIView):
def get_serializer_class(self):
if self.request.GET['flag']:
return SerializerA
return SerializerB
use inheritance to make serializers DRY.
My problem was somewhat similar to yours and I solved it with inheritance.
class StaticSerializer(serializers.ModelSerializer):
class Meta:
model = StaticModel
fields = (
'first_name', 'last_name', 'password', 'username',
'email'
)
class DynamicSerializer(StaticSerializer):
class Meta:
model = StaticModel
fields = (
'first_name',
)
Solution (ViewSet mixin)
I have solved this problem by writing my own ViewSet mixin. It provides quite easy and DRY way to override serializers depending on request action.
class ActionBasedSerializerClassMixin(viewsets.ModelViewSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def get_serializer_class(self):
attr_name = f'{self.action}_serializer_class'
if hasattr(self, attr_name):
serializer_class = getattr(self, attr_name)
self.serializer_class = serializer_class
return super().get_serializer_class()
Usage
To use this mixin inherit from it at your viewset (It must be before ModelViewSet parent).
The default serializer is always used as fallback
To use different serializer on list action just set attribute list_serializer_class at your viewset:
class MyViewSet(ViewSet):
serializer_class = MySerializer
list_serializer_class = MyListSerializer
With this code you will have MyListSerializer when action is 'list' and MySerializer for all other actions.
The same patterns works for all other action types: list, create, retrieve, update, partial_update, destroy.
You just need to append _serializer_class to get desired attribute name.
How serailizers should look like
class MySerializer(serializers.ModelSerializer):
some_reverse_rel = MyOtherSerializer(many=True, read_only=True)
class Meta:
model = MyModel
fields = ['field1', 'field2', 'foo', 'bar', 'some_reverse_rel']
class MyListSerailizer(MySerializer): # Note that we inherit from previous serializer
some_reverse_rel = None # Getting rid of reverse relationship
class Meta(MySerializer.Meta):
fields = ['foo', 'bar', 'field1']

Possible to do an `in` `lookup_type` through the django-filter URL parser?

I'm using django-filter with django-rest-framework and I'm trying to instantiate a filter that accepts lists of numbers for filtering the query set down
class MyFilter(django_filters.FilterSet):
ids = django_filters.NumberFilter(name='id',lookup_type='in')
class Meta:
model = MyModel
fields = ('ids',)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_class = MyFilter
If I pass in a comma separated list of integers, the filter is ignored altogether.
If I pass in a single integer, it gets through django-filter into django's form validator and complains:
'Decimal' object is not iterable
Is there a way to create a django-filter object which can handle a list of integers and properly filter down the queryset?
For better or worse, I created a custom filter for this:
class IntegerListFilter(django_filters.Filter):
def filter(self,qs,value):
if value not in (None,''):
integers = [int(v) for v in value.split(',')]
return qs.filter(**{'%s__%s'%(self.name, self.lookup_type):integers})
return qs
Which is used like:
class MyFilter(django_filters.FilterSet):
ids = IntegerListFilter(name='id',lookup_type='in')
class Meta:
model = MyModel
fields = ('ids',)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_class = MyFilter
Now my interface accepts comma-delimited lists of integers.
I know this is an old post, but there is now a better solution. The change that makes it correct is posted here.
They added a BaseInFilter and a BaseRangeFilter. The documentation is here.
Big picture, BaseFilter checks for CSV, and then when mixed with another filter it does what you are asking. Your code can now be written like:
class NumberInFilter(filters.BaseInFilter, filters.NumberFilter):
pass
class MyModelViewSet(viewsets.ModelViewSet):
ids = NumberInFilter(name='id', lookup_expr='in')
class Meta:
model = MyModel
fields = ['ids']
Here's a complete solution:
from django_filters import Filter, FilterSet
from rest_framework.filters import DjangoFilterBackend
from rest_framework.viewsets import ModelViewSet
from .models import User
from .serializers import UserSerializer
class ListFilter(Filter):
def filter(self, qs, value):
if not value:
return qs
self.lookup_type = 'in'
values = value.split(',')
return super(ListFilter, self).filter(qs, values)
class UserFilter(FilterSet):
ids = ListFilter(name='id')
class Meta:
model = User
fields = ['ids']
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
queryset = User.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_class = UserFilter
According to a post in the django-filter issues:
from django_filters import Filter
from django_filters.fields import Lookup
class ListFilter(Filter):
def filter(self, qs, value):
return super(ListFilter, self).filter(qs, Lookup(value.split(u","), "in"))
I have personally used this without any issue in my projects, and it works without having to create a per-type filter.
Based on #yndolok answer I have come to a general solution. I think filtering by a list of ids is a very common task and therefore should be included in the FilterBackend:
class ListFilter(django_filters.Filter):
"""Class to filter from list of integers."""
def filter(self, qs, value):
"""Filter function."""
if not value:
return qs
self.lookup_type = 'in'
try:
map(int, value.split(','))
return super(ListFilter, self).filter(qs, value.split(','))
except ValueError:
return super(ListFilter, self).filter(qs, [None])
class FilterBackend(filters.DjangoFilterBackend):
"""A filter backend that includes ListFilter."""
def get_filter_class(self, view, queryset=None):
"""Append ListFilter to AutoFilterSet."""
filter_fields = getattr(view, 'filter_fields', None)
if filter_fields:
class AutoFilterSet(self.default_filter_set):
ids = ListFilter(name='id')
class Meta:
model = queryset.model
fields = list(filter_fields) + ["ids"]
return AutoFilterSet
else:
return super(FilterBackend, self).get_filter_class(view, queryset)
Uptodate solution:
from django_filters import rest_framework as filters
name-->field_name
lookup_type-->lookup_expr
class IntegerListFilter(filters.Filter):
def filter(self,qs,value):
if value not in (None,''):
integers = [int(v) for v in value.split(',')]
return qs.filter(**{'%s__%s'%(self.field_name, self.lookup_expr):integers})
return qs
class MyFilter(filters.FilterSet):
ids = IntegerListFilter(field_name='id',lookup_expr='in')
class Meta:
model = MyModel
fields = ('ids',)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_class = MyFilter
As I have answered here DjangoFilterBackend with multiple ids, it is now pretty easy to make a filter that accepts list and validates the contents
For Example:
from django_filters import rest_framework as filters
class NumberInFilter(filters.BaseInFilter, filters.NumberFilter):
pass
class MyFilter(filters.FilterSet):
id_in = NumberInFilter(field_name='id', lookup_expr='in')
class Meta:
model = MyModel
fields = ['id_in', ]
This will accept a list of integers from a get parameter. For example /endpoint/?id_in=1,2,3

Resources