I am currently using Django Rest Framework and I am looking for a way to reuse some attributes from an already defined Serializer. In order to explain, I am going to expose some of the serializers involved:
Completed Serializer:
class ProductSerializer(serializers.ModelSerializer):
subscribed = serializers.SerializerMethodField()
other_field = serializers.SerializerMethodField()
class Meta:
model = Product
fields = [
'id',
'name',
'subscribed',
'other_field',
'x',
'y',
'z'
]
def get_subscribed(self, product: Product):
return product.is_user_subscribed(self.context['request'].user)
Simplified Serializer:
class ProductSimplifiedSerializer(serializers.ModelSerializer):
subscribed = serializers.SerializerMethodField()
class Meta:
model = Product
fields = [
'id',
'name',
'subscribed'
]
def get_subscribed(self, product: Product):
return product.is_user_subscribed(self.context['request'].user)
As you can notice above, those serializers are almost the same, but one of them is a simplified version of the object because I don't want to retrieve unnecessary information in some parts. The problem here is that in this scenario we have a method serializer that will need to be maintained two times. Maybe in the future, I would want to add another field, and I will need to add it to both. So, how could be achieved a Based Serializer in which all the fields are included but I could reuse it and extract specific fields from it?
I have already thought these options:
1. Using Base Serializer instance:
Draft:
def to_representation(self, instance)
desired_format = self.context['format']
if desired_format == 'simplified':
fields = ['fields_for_simplified']
elif desired_format == 'regular':
fields = ['fields_for_regular']
for field in fields:
# make the representation
With this approach, I do not know even if it would be a good idea or it is possible
Using SerializerField
Draft:
class UserSubscribed(serializer.SerializerField)
def to_representation(self, instance):
return 'representation'
class ProductSimplifiedSerializer(serializers.ModelSerializer):
user_subscribed = UserSubscribed()
class Meta:
model = Product
fields = [
'id',
'name',
'user_subscribed'
]
I think this last one is better, the problem is that this _user_subscribed_ is not a property of the Product instance and it is failing because of that and I do not know how to achieve this.
What would be your suggestion regarding this situation? Any advice would be appreciated.
Thanks!
You can subclass the simple one and add fields in the full one.
class ProductSimplifiedSerializer(serializers.ModelSerializer):
subscribed = serializers.SerializerMethodField()
class Meta:
model = Product
fields = [
'id',
'name',
'subscribed'
]
def get_subscribed(self, product: Product):
return product.is_user_subscribed(self.context['request'].user)
class ProductSerializer(ProductSimplifiedSerializer):
other_field = serializers.SerializerMethodField()
class Meta(ProductSimplifiedSerializer.Meta):
fields = ProductSimplifiedSerializer.Meta.fields + [
'other_field',
'x',
'y',
'z'
]
The trick here is that you can subclass the Meta class too. Any attributes you define in the Meta subclass will overwrite the parent ones, so you gotta manually use the parent ones.
You can create mixin for serializer class which would let you specify which fields will be "used" in serializer, any else will be ignored.
class DynamicFieldsMixin(object):
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super().__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
Then you can use your serializer like this:
class ProductSerializer(DynamicFieldsMixin, serializers.ModelSerializer)
serializer = ProductSerializer(fields=("id", "name", "subscribed"))
Related
I want to add a custom field that will only be used while creating an object. For example, I have a model serializer and I want to add that field that doesn't present in my model and I want to use this only while post request.
Model:
class Ship(BaseModel):
capacity = models.IntegerField(default=5)
mother_ship = models.ForeignKey(MotherShip, on_delete=models.CASCADE)
Serializer:
class ShipSerializer(serializers.ModelSerializer):
class Meta:
model = models.Ship
fields = ('id', 'mother_ship')
How I want:
class ShipSerializer(serializers.ModelSerializer):
count = serializers.SomeMagicalField() # only for post methods
class Meta:
model = models.Ship
fields = ('id', 'mother_ship')
There are serverals ways to achieve it, something easy is to create two serializer and in APIView implement the get_serializer() method, share the following example:
def get_serializer_class(self):
if not self.request.POST:
return ShipSerializerPOST
return ShipSerializer
The first solution can be applied when the serializers have big diferences.
In your case the diference is so small that you can use the second option that I share in the following example:
class ShipSerializer():
count = serializers.SomeMagicalField() # only for post methods
class Meta:
model = models.Ship
fields = ('id', 'mother_ship')
def __init__(self, *args, **kwargs):
# Don't return count if POST
if not self.request.POST:
del self.fields['count']
super().__init__(*args, **kwargs)
Hope one of the options helps.
I have a serializer
class CategoryListSerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ["id", "name", "name_en", "about", "parent",]
It is used in two locations:
All Categories API: Used to view rich details about the categories.
All Posts API: Used to know the name of the category only.
In my Posts Serializer, I used:
class PostListSerializer(serializers.ModelSerializer):
categories = CategoryListSerializer(many=True, )
class Meta:
model = Post
fields = ["id", "title", "description", "publish_date", "thumbnail", "owner", "categories", ]
And in my Post ViewSet:
class PostViewSet(ReadOnlyModelViewSet):
queryset = Post.objects.all().filter(is_published=True)
serializer_class = PostListSerializer
This returns All posts with All Categories Details mentioned in CategoryListSerializer, as it should be.
Question:
I want the PostListSerializer to return only the "name" field from the related Categories, without having to define another CategorySimpleSerializer that selects "name" field only. (I still need the CategoryListSerializer fields in another API)
Is it possible to do that?
Note: This is only an example, I'll have more usecases for this and want to know ahead if i'll have to create many custom "to-be-nested" Serialzers, to avoid exposing some unnecessary data to some of the APIs. It seemed like lots of redundant update work if a model or API needs change later.
As Mentioned by #mtzd in the comments:
Creating a generic Dynamic Serializer Class (As in DRF Docs here) worked!
My Category Serializer Looks like this now:
class DynamicFieldsCategorySerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super().__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
class CategoryListSerializer(DynamicFieldsCategorySerializer):
class Meta:
model = Category
fields = [ "name", "name_en", "about",]
and in PostListSerializer Categories variable, I added the fields attribute only:
categories = CategoryListSerializer(many=True, fields=['name',])
So now I can manage what fields to show to each View API i use, from a Single Model Serializer that i can modify/update once.
You should use serializers as described below for your use cases.
class PostListSerializer(serializers.ModelSerializer):
categories = serializers.SerializerMethodField('get_categories')
class Meta:
model = Post
fields = ["id", "title", "description", "publish_date", "thumbnail", "owner", "categories", ]
def get_categories(self, obj):
return obj.categories.all().values("name")
Also you need to optimize your
Post.objects.all().filter(is_published=True) to Post.objects.filter(is_published=True).select_related("categories")
class Meta:
read_only_fields = (
"id",
"slug",
)
Consider this case where I have a Book and Author model.
serializers.py
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = models.Author
fields = ('id', 'name')
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True)
class Meta:
model = models.Book
fields = ('id', 'title', 'author')
viewsets.py
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
This works great if I send a GET request for a book. I get an output with a nested serializer containing the book details and the nested author details, which is what I want.
However, when I want to create/update a book, I have to send a POST/PUT/PATCH with the nested details of the author instead of just their id. I want to be able to create/update a book object by specifying a author id and not the entire author object.
So, something where my serializer looks like this for a GET request
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True)
class Meta:
model = models.Book
fields = ('id', 'title', 'author')
and my serializer looks like this for a POST, PUT, PATCH request
class BookSerializer(serializers.ModelSerializer):
author = PrimaryKeyRelatedField(queryset=Author.objects.all())
class Meta:
model = models.Book
fields = ('id', 'title', 'author')
I also do not want to create two entirely separate serializers for each type of request. I'd like to just modify the author field in the BookSerializer.
Lastly, is there a better way of doing this entire thing?
There is a feature of DRF where you can dynamically change the fields on the serializer http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields
My use case: use slug field on GET so we can see nice rep of a relation, but on POST/PUT switch back to the classic primary key update. Adjust your serializer to something like this:
class FooSerializer(serializers.ModelSerializer):
bar = serializers.SlugRelatedField(slug_field='baz', queryset=models.Bar.objects.all())
class Meta:
model = models.Foo
fields = '__all__'
def __init__(self, *args, **kwargs):
super(FooSerializer, self).__init__(*args, **kwargs)
try:
if self.context['request'].method in ['POST', 'PUT']:
self.fields['bar'] = serializers.PrimaryKeyRelatedField(queryset=models.Bar.objects.all())
except KeyError:
pass
The KeyError is sometimes thrown on code initialisation without a request, possibly unit tests.
Enjoy and use responsibly.
IMHO, multiple serializers are only going to create more and more confusion.
Rather I would prefer below solution:
Don't change your viewset (leave it default)
Add .validate() method in your serializer; along with other required .create or .update() etc. Here, real logic will go in
validate() method. Where based on request type we will be creating
validated_data dict as required by our serializer.
I think this is the cleanest approach.
See my similar problem and solution at DRF: Allow all fields in GET request but restrict POST to just one field
You are looking for the get_serializer_class method on the ViewSet. This allows you to switch on request type for which serializer that you want to use.
from rest_framework import viewsets
class MyModelViewSet(viewsets.ModelViewSet):
model = MyModel
queryset = MyModel.objects.all()
def get_serializer_class(self):
if self.action in ('create', 'update', 'partial_update'):
return MySerializerWithPrimaryKeysForCreatingOrUpdating
else:
return MySerializerWithNestedData
I know it's a little late, but just in case someone else needs it. There are some third party packages for drf that allow dynamic setting of included serializer fields via the request query parameters (listed in the official docs: https://www.django-rest-framework.org/api-guide/serializers/#third-party-packages).
IMO the most complete ones are:
https://github.com/AltSchool/dynamic-rest
https://github.com/rsinger86/drf-flex-fields
where (1) has more features than (2) (maybe too many, depending on what you want to do).
With (2) you can do things such as (extracted from the repo's readme):
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['name', 'population']
class PersonSerializer(FlexFieldsModelSerializer):
country = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Person
fields = ['id', 'name', 'country', 'occupation']
expandable_fields = {
'country': (CountrySerializer, {'source': 'country', 'fields': ['name']})
}
The default response:
{
"id" : 13322,
"name" : "John Doe",
"country" : 12,
"occupation" : "Programmer"
}
When you do a GET /person/13322?expand=country, the response will change to:
{
"id" : 13322,
"name" : "John Doe",
"country" : {
"name" : "United States"
},
"occupation" : "Programmer",
}
Notice how population was ommitted from the nested country object. This is because fields was set to ['name'] when passed to the embedded CountrySerializer.
This way you can keep your POST requests including just an id, and "expand" GET responses to include more details.
The way I ended up dealing with this problem was having another serializer for when it's a related field.
class HumanSerializer(PersonSerializer):
class Meta:
model = Human
fields = PersonSerializer.Meta.fields + (
'firstname',
'middlename',
'lastname',
'sex',
'date_of_birth',
'balance'
)
read_only_fields = ('name',)
class HumanRelatedSerializer(HumanSerializer):
def to_internal_value(self, data):
return self.Meta.model.objects.get(id=data['id'])
class PhoneNumberSerializer(serializers.ModelSerializer):
contact = HumanRelatedSerializer()
class Meta:
model = PhoneNumber
fields = (
'id',
'contact',
'phone',
'extension'
)
You could do something like this, but for the RelatedSerializer do:
def to_internal_value(self, data):
return self.Meta.model.objects.get(id=data)
Thus, when serializing, you serialize the related object, and when de-serializing, you only need the id to get the related object.
I'm using Django 1.6 (very soon upgrading to 1.8), Python 2.7, and DRF 3.2.5 (very soon upgrading to latest).
I've got a set of deeply nested serializers (~10 levels deep, with a total of 20-30 models that are serialized).
I'm trying to add a boolean flag to the context, which will determine whether the serialized output hierarchy will be detailed (include all models' fields) or basic (part of the fields only).
I wrote the following code (partial snippet):
from rest_framework import serializers
from app.models import Institute, Department, Member
class MemberSerializer(serializers.ModelSerializer):
def get_fields(self):
fields = super(MemberSerializer, self).get_fields()
if self.context['basic_view']:
for field in ['height', 'weight']:
del fields[field]
return fields
class Meta:
model = Member
fields = ('id', 'birth_date', 'height', 'weight')
class DepartmentSerializer(serializers.ModelSerializer):
members = MemberSerializer(many=True, read_only=True)
def get_fields(self):
fields = super(DepartmentSerializer, self).get_fields()
if self.context['basic_view']:
for field in ['type', 'manager']:
del fields[field]
return fields
class Meta:
model = Department
fields = ('id', 'name', 'type', 'manager', 'members')
class InstituteSerializer(serializers.ModelSerializer):
departments = DepartmentSerializer(many=True, read_only=True)
def get_fields(self):
fields = super(InstituteSerializer, self).get_fields()
if self.context['basic_view']:
for field in ['name', 'type']:
del fields[field]
return fields
class Meta:
model = Institute
fields = ('id', 'name', 'type', 'departments')
def get_entities(is_basic_view):
institutes_list = Institute.objects.all()
serializer = InstituteSerializer(institutes_list, many=True, read_only=True, context={'basic_view': is_basic_view})
return serializer.data
But then found out that the 'context' that is passed from 'get_entities' to 'InstituteSerializer' is not passed-on to the nested serializers.
Meaning that in the example above - InstituteSerializer has 'basic_view' in the 'context', but MemberSerializer & DepartmentSerializer don't.
I found a working solution in context in nested serializers django rest framework : to use SerializerMethodField per nested field (e.g. 'departments'), and in the 'get_' method to manually pass-on the context.
My problem with that solution is that it requires embedding this code 20-30 times in my code, eventually doubling the number of source lines.
My request - if someone has (or can help implement) an extension for serializers.ModelSerializer, which will get an additional parameter upon construction, e.g. 'inherit_context'.
Then the only thing I'll need to change in my classes, for example in 'InstituteSerializer', is the addition of that parameter:
class InstituteSerializer(serializers.ModelSerializer):
departments = DepartmentSerializer(many=True, read_only=True, inherit_context=True)
def get_fields(self):
fields = super(InstituteSerializer, self).get_fields()
if self.context['basic_view']:
for field in ['name', 'type']:
del fields[field]
return fields
class Meta:
model = Institute
fields = ('id', 'name', 'type', 'departments')
Apparently I missed something...
The 'context' is already inherited down to the nested serializers...
However, the reason it didn't work for me, is because as part of my nesting, some of the child serializers were defined via serializers.SerializerMethodField().
And in such as case (only!) the context is not automatically inherited.
The solution is to simply pass-on the 'context', within the 'get_...' method related to each SerializerMethodField:
class ParentSerializer(serializers.ModelSerializer):
child = serializers.SerializerMethodField()
def get_child(self, obj):
child = ....
serializer = ChildSerializer(instance=child, context=self.context)
return serializer.data
P.S - a DRF github issue similar to mine was created a while ago: https://github.com/tomchristie/django-rest-framework/issues/2555
A serializer in my code is used at two different places.
First Use : Direct
class FirstSerializer(serializers.ModelSerializer):
class Meta(object):
model = FirstSerializer
fields = ('first_name', 'last_name', 'line1', 'line2',)
Second Use : Within another serializer
class SecondSerializer(serializers.ModelSerializer):
first_serilizer = FirstSerializer(many=True, read_only=True)
class Meta(object):
model = SecondSerializer
In first usecase, I need all fields.
In second usecase, while using it as nested object I want to exclude 'line2' from the list of fields.
I will subclass the 'FirstSerializer' class to limit code duplication and specify only the needed fields on the 'PartialFirstSerializer',
class FirstSerializer(serializers.ModelSerializer):
class Meta:
model = FirstModel
class PartialFirstSerializer(FirstSerializer):
class Meta:
fields = ('first_name', 'last_name', 'line1')
class SecondSerializer(serializers.ModelSerializer):
first_serializer = PartialFirstSerializer(many=True, read_only=True)
class Meta:
model = SecondModel
If you are looking for a more generic solution the DRF docs shows you how to create a DynamicFieldsModelSerializer, that takes a fields attribute.
http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields.keys())
for field_name in existing - allowed:
self.fields.pop(field_name)
class FirstSerializer(DynamicFieldsModelSerializer):
class Meta:
model = FirstModel
class SecondSerializer(serializers.ModelSerializer):
first_serializer = FirstSerializer(fields=('first_name', 'last_name', 'line1'), many=True, read_only=True)
class Meta:
model = SecondModel