How to nest two serializers with same model - python

I have two serializers with same model. I want to nest them.
Unfortunately this approach does not work:
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['name', 'word_count']
class BetterBookSerializer(serializers.ModelSerializer):
book = BookSerializer(many=False)
class Meta:
model = Book
fields = ('id', 'book')
Expected result:
{
"id": 123,
"book": {
"name": "book_name",
"word_count": 123
}
}

Use source=* instead of many=True as
class BetterBookSerializer(serializers.ModelSerializer):
book = BookSerializer(source='*')
class Meta:
model = Book
fields = ('id', 'book')
From the doc,
The value source='*' has a special meaning, and is used to indicate that the entire object should be passed through to the field. This can be useful for creating nested representations, or for fields which require access to the complete object in order to determine the output representation.

You can achieve the desired output like this:
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['name', 'word_count']
class BetterBookSerializer(serializers.ModelSerializer):
book = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Book
fields = ('id', 'book')
def get_book(self, obj):
return BookSerializer(obj).data
Small Update:
Although my approach to solve your problem works just fine, the answer from #JPG mentioning source='*' option is a good way to go. In that way you can easily use the nested serializer when creating new object.

Related

Django recursive serializing

Let's say I have models like this:
class A(models.Model):
...some fields here
class B(models.Model):
...some fields here
a = models.ForeignKey(A, on_delete=CASCADE)
class C(models.Model):
...some fields here
b = models.ForeignKey(B, on_delete=CASCADE)
...
And I want my API endpoint to return something like this
{
...some fields here
b: [
{
...some field here
c: [{...}, {...} ...]
},
{
...some field here
c: [{...}, {...} ...]
}
...
]
}
I know I can do something like this:
class Bserializer(serializers.ModelSerializer):
c = Cserializer(source="c_set", many=True, read_only=True,)
class Meta:
model = B
fields = [...some fields, "c"]
class Aserializer(serializers.ModelSerializer):
b = Bserializer(source="b_set", many=True, read_only=True,)
class Meta:
model = A
fields = [...some fields, "b"]
But if this goes deeper or/and models have more foreign keys it starts to become really complicated. Is there a way to add recursively all instances referencing the model.
You can specify a depth value in the serializer Meta class, however, this won't' work if you want to customize any part of the nested data, as this will automatically create model serializers (with all fields) for nested relations. Check docs here
class Aserializer(serializers.ModelSerializer):
class Meta:
model = A
fields = [...some fields, "b"]
depth = 2
The bottom line, if you don't do any customization of the nested serializers, use it, otherwise, stick with custom serializers for each of the relations. But I'll suggest keeping the nested data at a minimum as it will impact performance

Trying to do an INNER Join and display the result on the API

This question has been asked before but I cannot use any of the answers to my case.
I'm trying to have the equivalent of this, to show the results on the API.
SELECT denom_name,retail_name,retail_adr
FROM denomination d INNER JOIN Retailer r
ON r.id = d.retailer.id
These are my models (models.py):
class Retailer(models.Model):
retail_name = models.CharField(max_length=30)
retail_addr = models.CharField(max_length=300,null=True)
def __str__(self):
return self.retail_name
class Denomination(models.Model):
denom_name = models.CharField(max_length=1000)
retailer = models.ForeignKey(Retailer, on_delete=models.CASCADE)
I've created a viewset on the views.py
class DenomRetailViewset(viewsets.ModelViewSet):
queryset = Denomination.objects.select_related('Retailer')
serializer_class = DenomRetailSerializer
But here lies the issue, at least one of them.
I'm creating the serializer through the serializer.py
class DenomRetailSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model= retailer,denomination
fields = ('denom_name','retail_name','retail_adr')
But as you can see, the serializer cannot accept two models. And beside, I have doubts about the viewset, queryset = Denomination.objects.select_related('Retailer').
Any tips are more than welcomed as I'm starting to lose my sanity.
Thanks.
Use source--DRF doc argument
class DenomRetailSerializer(serializers.HyperlinkedModelSerializer):
retail_name = serializers.CharField(source='retailer.retail_name')
retail_adr = serializers.CharField(source='retailer.retail_adr')
class Meta:
model = Denomination
fields = ('denom_name', 'retail_name', 'retail_adr')
Also, it should be .select_related('retailer') instead of .select_related('Retailer')
You can use "depth" in this case:
class DenomRetailSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model= denomination
fields = ('denom_name','retail')
depth = 1
The response will return an inner join like that:
{
"id": 1,
"denom_name ": "...",
"retail": {
"id" : 1,
"retail_name" : "...",
"retail_addr" : "..."
}
}

Django Rest Framework: How to modify output structure?

Is there a way to group fields in Serializer/ModelSerializer or to modify JSON structure?
There is a Location model:
class Location(Model):
name_en = ...
name_fr = ...
...
If I use ModelSerializer I get plain representation of the object fields like:
{'name_en':'England','name_fr':'Angleterre'}
I want to group some fields under "names" key so I get
{'names':{'name_en':'England','name_fr':'Angleterre'}}
I know I can create custom fields but I want to know if there is a more straightforward way. I tried
Meta.fields = {'names':['name_en','name_fr']...}
which doesn't work
I think it is better using a property. Here is the whole example.
class Location(models.Model):
name_en = models.CharField(max_length=50)
name_fr = models.CharField(max_length=50)
#property
def names(self):
lst = {field.name: getattr(self, field.name)
for field in self.__class__._meta.fields
if field.name.startswith('name_')}
return lst
In views:
class LocationViewSet(viewsets.ModelViewSet):
model = models.Location
serializer_class = serializers.LocationSerializer
queryset = models.Location.objects.all()
And in serializers:
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = ('id', 'names')
My result for my fake data:
[{
"id": 1,
"names": {
"name_en": "England",
"name_fr": "Angleterre"}
}]
Try to create a wrapper serialzer and place the LocationSerializer inside it
class LocationSerialzer(serializers.ModelSerialzer):
name_en = ...
name_fr = ...
...
class MySerializer(serializers.ModelSerializer):
name = LocationSerialzer()
...
Using the above method , you can apply your own customization without being limited to drf custom fields.
You could also not use a property on the model and but use a SerializerMethodField on your serializer like in this implementation.
We used here a _meta.fields, like in the other implementation, to get all the fields that starts with name_ so we can dynamically get the output you desired
class LocationSerializer(serializers.ModelSerializer):
names = serializers.SerializerMethodField()
def get_names(self, obj):
lst = {field.name: getattr(obj, field.name)
for field in obj.__class__._meta.fields
if field.name.startswith('name_')}
return lst
class Meta:
model = Location
fields = ('id', 'names')

Modifying value on serialization - Django Rest Framework

I have a model which contains sensitive data, let's say a social security number, I would like to transform that data on serialization to display only the last four digits.
I have the full social security number stored: 123-45-6789.
I want my serializer output to contain: ***-**-6789
My model:
class Employee (models.Model):
name = models.CharField(max_length=64,null=True,blank=True)
ssn = models.CharField(max_length=16,null=True,blank=True)
My serializer:
class EmployeeSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
class Meta:
model = Employee
fields = ('id','ssn')
read_only_fields = ['id']
You can use SerializerMethodField:
class EmployeeSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
ssn = SerializerMethodField()
class Meta:
model = Employee
fields = ('id','ssn')
read_only_fields = ['id']
def get_ssn(self, obj):
return '***-**-{}'.format(obj.ssn.split('-')[-1]
If you don't need to update the ssn, just shadow the field with a SerializerMethodField and define get_ssn(self, obj) on the serializer.
Otherwise, the most straightforward way is probably to just override .to_representation():
def to_representation(self, obj):
data = super(EmployeeSerializer, self).to_representation(obj)
data['ssn'] = self.mask_ssn(data['ssn'])
return data
Please add special case handling ('ssn' in data) as necessary.
Elaborating on #dhke’s answer, if you want to be able to reuse this logic to modify serialization across multiple serializers, you can write your own field and use that as a field in your serializer, such as:
from rest_framework import serializers
from rest_framework.fields import CharField
from utils import mask_ssn
class SsnField(CharField):
def to_representation(self, obj):
val = super().to_representation(obj)
return mask_ssn(val) if val else val
class EmployeeSerializer(serializers.ModelSerializer):
ssn = SsnField()
class Meta:
model = Employee
fields = ('id', 'ssn')
read_only_fields = ['id']
You can also extend other fields like rest_framework.fields.ImageField to customize how image URLs are serialized (which can be nice if you’re using an image CDN on top of your images that lets you apply transformations to the images).

django rest framework abstract class serializer

I have some models like these:
class TypeBase(models.Model):
name = models.CharField(max_length=20)
class Meta:
abstract=True
class PersonType(TypeBase):
pass
class CompanyType(TypeBase):
pass
Having this, I want to create just one serializer that holds all these field types (serialization, deserialization, update and save).
To be more specific, I want only one serializer (TypeBaseSerializer) that print the Dropdown on the UI, serialize the json response, deserialize it on post and save it for all my based types.
Something like this:
class TypeBaseSerializer(serializers.Serializer):
class Meta:
model = TypeBase
fields = ('id', 'name')
Is it possible?
I think the following approach is more cleaner. You can set "abstract" field to true for the base serializer and add your common logic for all child serializers.
class TypeBaseSerializer(serializers.ModelSerializer):
class Meta:
model = TypeBase
fields = ('id', 'name')
abstract = True
def func(...):
# ... some logic
And then create child serializers and use them for data manipulation.
class PersonTypeSerializer(TypeBaseSerializer):
class Meta:
model = PersonType
fields = ('id', 'name')
class CompanyTypeSerializer(TypeBaseSerializer):
class Meta:
model = CompanyType
fields = ('id', 'name')
Now you can use the both of these serializers normally for every model.
But if you really want to have one serializers for both the models, then create a container model and a serializer for him too. That is much cleaner :)
You can't use a ModelSerializer with an abstract base model.
From restframework.serializers:
if model_meta.is_abstract_model(self.Meta.model):
raise ValueError(
'Cannot use ModelSerializer with Abstract Models.'
)
I wrote a serializer_factory function for a similar problem:
from collections import OrderedDict
from restframework.serializers import ModelSerializer
def serializer_factory(mdl, fields=None, **kwargss):
""" Generalized serializer factory to increase DRYness of code.
:param mdl: The model class that should be instanciated
:param fields: the fields that should be exclusively present on the serializer
:param kwargss: optional additional field specifications
:return: An awesome serializer
"""
def _get_declared_fields(attrs):
fields = [(field_name, attrs.pop(field_name))
for field_name, obj in list(attrs.items())
if isinstance(obj, Field)]
fields.sort(key=lambda x: x[1]._creation_counter)
return OrderedDict(fields)
# Create an object that will look like a base serializer
class Base(object):
pass
Base._declared_fields = _get_declared_fields(kwargss)
class MySerializer(Base, ModelSerializer):
class Meta:
model = mdl
if fields:
setattr(Meta, "fields", fields)
return MySerializer
You can then use the factory to produce serializers as needed:
def typebase_serializer_factory(mdl):
myserializer = serializer_factory(
mdl,fields=["id","name"],
#owner=HiddenField(default=CurrentUserDefault()),#Optional additional configuration for subclasses
)
return myserializer
Now instanciate different subclass serializers:
persontypeserializer = typebase_serializer_factory(PersonType)
companytypeserializer = typebase_serializer_factory(CompanyType)
As already mentioned in Sebastian Wozny's answer, you can't use a ModelSerializer with an abstract base model.
Also, there is nothing such as an abstract Serializer, as some other answers have suggested. So setting abstract = True on the Meta class of a serializer will not work.
However you need not use use a ModelSerializer as your base/parent serializer. You can use a Serializer and then take advantage of Django's multiple inheritance. Here is how it works:
class TypeBaseSerializer(serializers.Serializer):
# Need to re-declare fields since this is not a ModelSerializer
name = serializers.CharField()
id = serializers.CharField()
class Meta:
fields = ['id', 'name']
def someFunction(self):
#... will be available on child classes ...
pass
class PersonTypeSerializer(TypeBaseSerializer, serializers.ModelSerializer):
class Meta:
model = PersonType
fields = TypeBaseSerializer.Meta.fields + ['another_field']
class CompanyTypeSerializer(TypeBaseSerializer, serializers.ModelSerializer):
class Meta:
model = CompanyType
fields = TypeBaseSerializer.Meta.fields + ['some_other_field']
So now since the fields name and id are declared on the parent class (TypeBaseSerializer), they will be available on PersonTypeSerializer and since this is a child class of ModelSerializer those fields will be populated from the model instance.
You can also use SerializerMethodField on the TypeBaseSerializer, even though it is not a ModelSerializer.
class TypeBaseSerializer(serializers.Serializer):
# you will have to re-declare fields here since this is not a ModelSerializer
name = serializers.CharField()
id = serializers.CharField()
other_field = serializers.SerializerMethodField()
class Meta:
fields = ['id', 'name', 'other_field']
def get_other_field(self, instance):
# will be available on child classes, which are children of ModelSerializers
return instance.other_field
Just iterating a bit over #adki's answer:
it is possible to skip model for TypeBaseSerializer;
derived serializers can refer to TypeBaseSerializer.Meta, so you would change them in a single place.
class TypeBaseSerializer(serializers.Serializer):
class Meta:
fields = ('id', 'name', 'created')
abstract = True
def func(...):
# ... some logic
class PersonTypeSerializer(TypeBaseSerializer):
class Meta:
model = PersonType
fields = TypeBaseSerializer.Meta.fields + ('age', 'date_of_birth')
class CompanyTypeSerializer(TypeBaseSerializer):
class Meta:
model = CompanyType
fields = TypeBaseSerializer.Meta.fields

Categories