How to dynamically remove fields from serializer output - django-rest-framework

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']

Related

Djangorestframework, how can I use a serializer with custom fields that I want passed for the creation method?

Let's say I have the following:
class EntityManager(Manager):
def create(label, entity_type, **kwargs):
... do stuff with label and entity type
obj = super().create(**cleanedupkwargs)
obj.addstuffwithlabel(label)
return obj
class Entity(Model):
somefields...
objects = EntityManager()
There's no problem with this and I can call Entity.objects.create(label='foo', entity_type=my_entity_type, other_params=foo)
the issue is I'm now using a serializer and I tried this
class EntityBareboneSerializer(serializers.ModelSerializer):
label = serializers.SerializerMethodField()
entity_type = serializers.SerializerMethodField()
class Meta:
model = Entity
fields = [
'id',
'label',
'entity_type',
]
def validate_label(self, label):
return label
def validate_entity_type(self, entity_type):
return entity_type
def create(self, validated_data):
# do stuff with label and entity type
return Entity.objects.create(**validated_data)
The issue is when is_valid is called the validated_data param comes back empty.
Any idea if it's possible to effectively use my custom create method in the serializer?
You can pre-process the validated data, before creating an instance
def create(self, validated_data):
label = validated_data.pop("label", "some_default_value")
entity_type = validated_data.pop("entity_type", "some_default_value")
obj = Entity.objects.create(**validated_data)
obj.addstuffwithlabel(label)
return obj

Django: customizing the field types needed for create and retrieve serializers

I currently have the following serializer:
serializers.py
class SurfGroupSerializer(serializers.ModelSerializer):
instructor = SurfInstructorSerializer(many=False)
surfers = SurferSerializer(many=True)
class Meta:
model = SurfGroup
fields = ['uuid', 'instructor', 'date', 'starting_time', 'ending_time', 'surfers']
def create(self, validated_data):
return SurfGroup(**validated_data)
And the following viewset create method (viewset inherited from viewsets.ViewSet as we need some bespoke customization, extra signals and actions etc):
viewsets.py
# Surf Group Create View:
def create(self, request, format=None):
serializer = SurfGroupSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
response = responses.standardized_json_response(
message='Surf Group Objects Have Been Successfully Created',
data=serializer.data
)
return Response(data=response, status=status.HTTP_201_CREATED, headers=headers)
For the retrieve action, the serializer works well, and we have a nested instructor object in the response. However, I want to perform a create by passing in the instructor uuid attrbiute like (see content in the POST textarea):
Rather than a whole object...I was wondering how we achieve this? Is it best to just have two Serializers, one for performing the create, and one the retrieval?
def create(self, validated_data):
surf_group = SurfGroup(
instructor__uuid=validated_data['instructor'],
)
surf_group.save()
return surf_group
It is good question.
I work with this situations many times and it looks like one option is to have two serializers as you mean: 1 for list/retrieve and 1 for save.
Another option (for me) is to set serializer field input as UUID and output as another serializer data like this:
class SurfGroupSerializer(serializers.ModelSerializer):
instructor = serializers.UUIDField()
surfers = SurferSerializer(many=True, read_only=True)
class Meta:
model = SurfGroup
fields = ['uuid', 'instructor', 'date', 'starting_time', 'ending_time', 'surfers']
# I use this validate method to transform uuid to object which will
# be bypassed to create method for easly save
def validate_instructor(self, instructor_uuid):
try:
return Instructor.objects.get(uuid=instructor_uuid)
except Instructor.DoesNotExist:
# Remember that you dont need to pass field_key: [errors] to ValidationError
# because validate_FIELD will automatically pass field_key for you !
raise ValidationError(['Instructor with the given uuid does not exist.'])
# Overwrite output data
def to_representation(self, instance):
ret = super().to_representation(instance)
ret['instructor'] = SurfInstructorSerializer(instance=instance.instructor).data
return ret

Django REST serializer required field

I have a simple serializer with one required field:
class MySerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = '__all__'
read_only_fields = ('field1', 'field2')
In my model there is an 'url' field which is required to create new object (method: POST). I would like to set required: False for PUT method. How can I achieve that? Thanks for any clues...
I assume you want to change/set one or multiple fields of an existing MyModel instance.
In such case, you need to pass a partial=True keyword argument to serializer. Then even if you PUT or PATCH without url field in data, your serializer.is_valid() would evaluate to True.
https://www.agiliq.com/blog/2019/04/drf-polls/#edit-a-poll-question should help if my assumption about your question is correct.
I found this answer helpful: Django Rest Framework set a field read_only after record is created .
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance is not None:
self.fields.get('url').read_only = True
This code works fine.

Django REST Framework: passing context to a nested serializer

I've a pair of parent-child models/serializers/viewsets - Tool and ToolInput:
models.py:
class Tool(models.Model):
id = models.CharField(max_length=10000, primary_key=True, default=uuid.uuid4, editable=False)
base_command = jsonfield.JSONField(verbose_name="baseCommand")
class ToolInput(models.Model):
tool = models.ForeignKey(Tool, related_name="inputs", on_delete=models.CASCADE)
id = models.CharField(max_length=10000, primary_key=True)
label = models.CharField(max_length=10000, null=True, blank=True)
description = models.CharField(max_length=10000, null=True, blank=True)
type = jsonfield.JSONField()
serializers.py
class ToolSerializer(WritableNestedModelSerializerMixin,
serializers.HyperlinkedModelSerializer):
id = serializers.CharField()
inputs = ToolInputSerializer(many=True)
baseCommand = serializers.JSONField(source="base_command")
class Meta:
model = Tool
fields = ('id', 'inputs', 'baseCommand')
class ToolInputSerializer(WritableNestedModelSerializerMixin,
serializers.HyperlinkedModelSerializer):
class Meta:
model = ToolInput
fields = ('id', 'label', 'description', 'type')
views.py:
class ToolViewSet(viewsets.ModelViewSet):
queryset = Tool.objects.all()
lookup_field = 'id'
serializer_class = ToolSerializer
class ToolInputViewSet(viewsets.ModelViewSet):
lookup_field = 'id'
serializer_class = ToolInputSerializer
def get_queryset(self):
tool_id = self.kwargs['tool_id']
return ToolInput.objects.filter(tool_id=tool_id)
def get_serializer_context(self):
context = super(ToolInputViewSet, self).get_serializer_context()
context["tool"] = Tool.objects.get(id=self.kwargs['tool_id'])
return context
As you can see, I use ToolInputSerializer both as a standalone serializer for ToolInputViewSet and as a nested serializer within ToolViewSet.
When ToolInputSerializer is used as a nested serializer in ToolViewSet, it somehow automagically receives the value of tool argument and assigns it to ToolInput model's tool field (by the way, I feel that it's a totally wrong behavior from architectural point of view - there's no such field as tool on ToolInputSerializer at all and DRF's filling the respective model's field - it should bail out with a Field Does Not Exist error IMO and at least require a write-only field tool on serializer).
But when I use it as a standalone serializer in ToolInputViewSet, I want to assign the value of ToolInput model's tool field to Tool instance, determined by tool_id url parameter, received by ToolInputViewSet in kwargs.
I'm trying to pass the value of that field with serializer context, overriding ToolInputViewSet.get_serializer_context() method, but it's not working. How to do that properly?
Sidenote: I'm pretty tired of the messy and inconsistent, non-uniform automagic of DRF's context handling that pierces layers of Model-Serializer-Field-View architecture. It really needs to be more explicit and customizable.
As for the context, I still don't know how to make it work.
As for how nested serializers work, this is my bad: as you can see, I inherit all the ViewSets from my custom WritableNestedModelSerializerMixin, where I've overridden create() and update() methods to work with nested data structures, so this is my tinkering.
So, as a workaround, I created a separate StandaldonToolInputSerializer and modified ToolInputViewSet, adding the missing tool field to serializer and automatically patching request.data with the Tool reference:
serializers.py
class StandaloneToolInputSerializer(serializers.HyperlinkedModelSerializer):
tool = serializers.PrimaryKeyRelatedField(
write_only=True,
many=False,
queryset=Tool.objects.all()
)
inputBinding = serializers.JSONField(source="input_binding")
class Meta:
model = ToolInput
fields = ('id', 'tool', 'label', 'description', 'type', 'inputBinding')
views.py
class ToolInputViewSet(viewsets.ModelViewSet):
'''
Describes a Tool input.
'''
lookup_field = 'id'
serializer_class = StandaloneToolInputSerializer
def get_queryset(self):
tool_id = self.kwargs['tool_id']
return ToolInput.objects.filter(tool_id=tool_id)
def create(self, request, *args, **kwargs):
request.data["tool"] = self.kwargs['tool_id']
return super(ToolInputViewSet, self).create(request, *args, **kwargs)
def update(self, request, *args, **kwargs):
request.data["tool"] = self.kwargs['tool_id']
return super(ToolInputViewSet, self).update(request, *args, **kwargs)

Django 1.3 CreateView, ModelForm and filtering fields by request.user

I am trying to filter a field on a ModelForm. I am subclassing the generic CreateView for my view. I found many references to my problem on the web, but the solutions do not seem to work (for me at least) with Django 1.3's class-based views.
Here are my models:
#models.py
class Subscriber(models.Model):
user = models.ForeignKey(User)
subscriber_list = models.ManyToManyField('SubscriberList')
....
class SubscriberList(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=70)
....
Here is my view:
#views.py
class SubscriberCreateView(AuthCreateView):
model = Subscriber
template_name = "forms/app.html"
form_class = SubscriberForm
success_url = "/app/subscribers/"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
return super(SubscriberCreateView, self).form_valid(form)
Here is my original form for adding a Subscriber, with no filter:
#forms.py
class SubscriberForm(ModelForm):
class Meta:
model = Subscriber
exclude = ('user', 'facebook_id', 'twitter_id')
Here is my modified form, attempting to filter, but doesn't work:
#forms.py
class SubscriberForm(ModelForm):
class Meta:
model = Subscriber
exclude = ('user', 'facebook_id', 'twitter_id')
def __init__(self, user, **kwargs):
super(SubscriberForm, self).__init__(**kwargs)
self.fields['subscriber_list'].queryset = SubscriberList.objects.filter(user=user)
If I change this modified form as so:
def __init__(self, user=None, **kwargs)
It works - It brings me NO subscriber lists. But any way I try to pass the request user, I invariably get a a name "request" or name "self" not defined error.
So, how can I modify my code to filter subscriber_list by the request.user, and still use Django 1.3's CreateView.
I see you've been posting this question in various places.. and the way I found that is because I was trying to figure out the same thing. I think I just got it working, and here's what I did. I overwrote get_form() from FormMixin to filter a specific form fields queryset:
class MyCreateView(CreateView):
def get_form(self, form_class):
form = super(MyCreateView,self).get_form(form_class) #instantiate using parent
form.fields['my_list'].queryset = MyObject.objects.filter(user=self.request.user)
return form

Resources