Dynamically modifying serializer fields in Django Rest Framework - python

I'm trying to use the Advanced serializer usage described in the django rest framework documentation. http://django-rest-framework.org/api-guide/serializers.html#advanced-serializer-usage to dynamically modifying serializer field
Here is my serializer class:
class MovieSerializer(serializers.ModelSerializer):
moviework_work = MovieWorkSerializer(many=True)
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
dropfields = kwargs.pop('dropfields', None)
# Instantiate the superclass normally
super(MovieSerializer, self).__init__(*args, **kwargs)
if dropfields:
# Drop fields specified in the `fields` argument.
banished = set(dropfields)
for field_name in banished:
self.fields.pop(field_name)
class Meta:
model = Movie
fields = ('field1','field2','moviework_work')
Here is my viewset
class MovieFromInterpreterViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer(dropfields=('moviework_work',))
I get this error:
TypeError: 'MovieSerializer' object is not callable

Note that you are setting serializer_class not to a class, but to an instance of the class. You either need to set dropfields as an attribute on the class, (just like it does for fields in the documented example you link to) or you need to look at overriding the get_serializer method of the viewset (docs).

Related

Does Django have a way for ModelViewSet to represent both parent and child models?

I need to have extra fields in response if they are available, but not all objects of that class have this property. So for example we have
class Car(models.Model):
brand = model.CharField()
wheelcount = model.IntField()
class Truck(Car):
max_load = model.IntField()
class Bus(Car):
max_people = model.IntField()
and a view
class CarView(ReadOnlyModelViewSet):
serializer_class = CarSerializer
queryset = Car.objects.all()
And I want to have max_load and max_people when i check all available cars.
Is there a way to either write CarSerializer to somehow serialize child objects differently, or a way to make view class choose a serializer based on class or additional field(like having an enum CarType)?
You can specify which fields you'd like to serialize normally in the meta class.
ex:
class CarSerializer(serializers.ModelSerializer):
class Meta:
model = Car
fields = [<whatever specific fields you want to serialize>]
if you need to serialize objects based on certain conditions you can use the SerializerMethodField. https://www.django-rest-framework.org/api-guide/fields/#serializermethodfield
From django rest documentation:
This is a read-only field. It gets its value by calling a method on
the serializer class it is attached to. It can be used to add any sort
of data to the serialized representation of your object.
Signature: SerializerMethodField(method_name=None)
method_name - The name of the method on the serializer to be called.
If not included this defaults to get_<field_name>. The serializer
method referred to by the method_name argument should accept a single
argument (in addition to self), which is the object being serialized.
It should return whatever you want to be included in the serialized
representation of the object. For example:
from django.contrib.auth.models import User
from django.utils.timezone import now
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
days_since_joined = serializers.SerializerMethodField()
class Meta:
model = User
fields = '__all__'
def get_days_since_joined(self, obj):
return (now() - obj.date_joined).days
in your case:
class CarSerializer(serializers.ModelSerializer):
object = serializers.SerializerMethodField()
def get_object(self, instance):
if instance.CarType:
return <your desired object>

Is there an easy way to only serialize non-empty fields with Django Rest Framework's ModelSerializer?

I am working on a Django project with a number of rather large models (around 80 fields). I am using Django Rest Framework's ModelSerializer to serialize the models, and ViewSets to provide an API for my frontend.
That works very well, but I would like to reduce the amount of data that is being transferred by the server. Most of my model fields are optional and many instances only have values for a few of them. In those cases I would like to serialize only those fields that have values (i.e. that are truthy).
I imagine I could do that either on the serializer side or on the model side, but I do not quite understand how these two talk to each other, so to speak.
My current serializer is very simple:
class OutfitSerializer(serializers.ModelSerializer):
class Meta:
model = Outfit
fields = '__all__'
The view is equally simple:
# Outfit views
class OutfitViewSet(viewsets.ViewSet):
def list(self, request):
queryset = Outfit.objects.all()
serializer = OutfitSerializer(queryset, many=True)
return Response(serializer.data)
I fiddled with sub-classing the serializer and modifying the __init__ function (inspired by this part of the DRF docs):
class NonEmptyFieldsModelSerializer(serializers.ModelSerializer):
"""
ModelSerializer that allows fields to be set at runtime via the
optional 'fields' argument
Copied from https://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields
"""
def __init__(self, *args, **kwargs):
super(NonEmptyFieldsModelSerializer, self).__init__(*args, **kwargs)
all_fields = set(self.fields)
for field_name in all_fields:
# IF THIS FIELD IS EMPTY IN THE OBJECT CURRENTLY BEING SERIALIZED:
self.fields.pop(field_name)
but I am not sure how and whether I have access to the current object in the __init__. I also don't quite understand how that would work for serializing a whole queryset: Would a new serializer instance be initialized for each model instance?
I could simply write a serializer function for the model itself, but that would kind of defeat the purpose of using Django Rest Framework, as I would have to configure each field individually.
So, how can I serialize only non-empty fields of a model instance?
EDIT: I also wanted to remove decimal numbers with value 0. However, DRF's ModelSerializer converts decimals to strings by default in order to avoid inaccuracies. Therefore, I adjusted Igor's answer as follows:
class NonEmptySerializer(serializers.ModelSerializer):
def to_representation(self, instance):
ret = super().to_representation(instance)
non_null_ret = copy.deepcopy(ret)
for key in ret.keys():
if not ret[key]:
non_null_ret.pop(key)
elif isinstance(ret[key], str) and re.fullmatch('[0.]+', ret[key]):
non_null_ret.pop(key)
return non_null_ret
You can override the to_representation method of ModelSerializer:
class NonEmptySerializer(ModelSerializer):
def to_representation(self, instance):
ret = super().to_representation(instance)
non_null_ret = copy.deepcopy(ret)
for key in ret.keys():
if not ret[key]:
non_null_ret.pop(key)
return non_null_ret
Then inherit from this serialiser when needed:
class OutfitSerializer(NonEmptySerializer):
class Meta:
model = Outfit
fields = '__all__'
Since to_representation is called for both single and list serialisers, it works in both cases.

ListSerializer in Django Restful - When is it called?

I have the following code for my serializers.py:
from rest_framework import serializers
from django.db import transaction
from secdata_finder.models import File
class FileListSerializer(serializers.ListSerializer):
#transaction.atomic
def batch_save_files(file_data):
files = [File(**data) for data in file_data]
return File.objects.bulk_create(files)
def create(self, validated_data):
print("I am creating multiple rows!")
return self.batch_save_files(validated_data)
class FileSerializer(serializers.ModelSerializer):
class Meta:
list_serializer_class = FileListSerializer
model = File
fields = (...) # omitted
I'm experimenting with it on my Django test suite:
def test_file_post(self):
request = self.factory.post('/path/file_query', {"many":False})
request.data = {
... # omitted fields here
}
response = FileQuery.as_view()(request)
It prints I am creating multiple rows!, which is not what should happen.
Per the docs:
... customize the create or update behavior of multiple objects.
For these cases you can modify the class that is used when many=True is passed, by using the list_serializer_class option on the serializer Meta class.
So what am I not understanding? I passed in many:False in my post request, and yet it still delegates the create function to the FileListSerializer!
Per the docs:
The ListSerializer class provides the behavior for serializing and
validating multiple objects at once. You won't typically need to use
ListSerializer directly, but should instead simply pass many=True when
instantiating a serializer
You can add many=True to your serializer
class FileSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
kwargs['many'] = kwargs.get('many', True)
super().__init__(*args, **kwargs)
Potential dupe of How do I create multiple model instances with Django Rest Framework?

different validation in drf serializer per request method

Lets say i have a model like so:
class MyModel(models.Model):
first_field = models.CharField()
second_field = models.CharField()
and an API view like so:
class MyModelDetailAPI(GenericAPIView):
serializer_class = MyModelSerializer
def patch(self, request, *args, **kwargs):
# Do the update
def post(self, request, *args, **kwargs):
# Do the post
The first_field is a field that is only inserted in the POST method (and is mandatory) but on each update, the user can't change its value so the field in the PATCH method is not mandatory.
How can i write my serializer so that the first_field is required on POST but not required on PATCH. Is there any way of dynamically setting the required field so i can still use the DRF validation mechanism? Some sort of validator dispatcher per request method?
I want something like this for example:
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = {
'POST': ['first_field']
'PATCH': []
}
I need more space than comments provide to make my meaning clear. So here is what I suggest:
Different formatting means different serializers.
So here you have, for instance a MyModelSerializer and a MyModelCreationSerializer. Either create them independently, or have one inherit the other and specialize it (if it makes sense).
Use the appropriate GenericAPIView hook to return the correct serializer class depending on self.action. A very basic example could be:
class MyModelDetailAPI(GenericAPIView):
# serializer_class = unneeded as we override the hook below
def get_serializer_class(self):
if self.action == 'create':
return MyModelCreationSerializer
return MyModelSerializer
Default actions in regular viewsets are documented here, they are:
create: POST method on base route url
list: GET method on base route url
retrieve: GET method on object url
update: PUT method on object url
partial_update: PATCH method on object url
destroy: DELETE method on object url

Django REST Framework - Serializing optional fields

I have an object that has optional fields. I have defined my serializer this way:
class ProductSerializer(serializers.Serializer):
code = serializers.Field(source="Code")
classification = serializers.CharField(source="Classification", required=False)
I thought required=False would do the job of bypassing the field if it doesn't exist. However, it is mentioned in the documentation that this affects deserialization rather than serialization.
I'm getting the following error:
'Product' object has no attribute 'Classification'
Which is happening when I try to access .data of the serialized instance. (Doesn't this mean it's deserialization that's raising this?)
This happens for instances that do not have Classification. If I omit Classification from the serializer class it works just fine.
How do I correctly do this? Serialize an object with optional fields, that is.
Django REST Framework 3.0+
Dynamic fields now supported, see http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields -- this approach defines all of the fields in the serializer, and then allows you to selectively remove the ones you don't want.
Or you could also do something like this for a Model Serializer, where you mess around with Meta.fields in the serializer init:
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('code',)
def __init__(self, *args, **kwargs):
if SHOW_CLASSIFICATION: # add logic here for optional viewing
self.Meta.fields = list(self.Meta.fields)
self.Meta.fields.append('classification')
super(ProductSerializer, self).__init__(*args, **kwargs)
You'd have to ask Tom though if this is the "correct way" since it may not fit in with the long term plan.
Django REST Framework < 3.0
Try something like this:
class ProductSerializer(serializers.Serializer):
...
classification = serializers.SerializerMethodField('get_classification')
def get_classification(self, obj):
return getattr(obj, 'classification', None)
Multiple Serializers
Another approach would be to create multiple serializers with different sets of fields. One serializer inherits from another and adds additional fields. Then you can choose the appropriate serializer in the view with the get_serializer_class method. Here's an actual example of how I use this approach to call different serializers to present different user data if the user object is the same as the request user.
def get_serializer_class(self):
""" An authenticated user looking at their own user object gets more data """
if self.get_object() == self.request.user:
return SelfUserSerializer
return UserSerializer
Removing fields from representation
Another approach that I've used in security contexts is to remove fields in the to_representation method. Define a method like
def remove_fields_from_representation(self, representation, remove_fields):
""" Removes fields from representation of instance. Call from
.to_representation() to apply field-level security.
* remove_fields: a list of fields to remove
"""
for remove_field in remove_fields:
try:
representation.pop(remove_field)
except KeyError:
# Ignore missing key -- a child serializer could inherit a "to_representation" method
# from its parent serializer that applies security to a field not present on
# the child serializer.
pass
and then in your serializer, call that method like
def to_representation(self, instance):
""" Apply field level security by removing fields for unauthorized users"""
representation = super(ProductSerializer, self).to_representation(instance)
if not permission_granted: # REPLACE WITH PERMISSION LOGIC
remove_fields = ('classification', )
self.remove_fields_from_representation(representation, remove_fields)
return representation
This approach is straightforward and flexible, but it comes at the cost of serializing fields that are sometimes not displayed. But that's probably okay.
The method describe below did the work for me.
Pretty simple,easy and worked for me.
DRF version used = djangorestframework (3.1.0)
class test(serializers.Serializer):
id= serializers.IntegerField()
name=serializers.CharField(required=False,default='some_default_value')
The serializers are deliberately designed to use a fixed set of fields so you wouldn't easily be able to optionally drop out one of the keys.
You could use a SerializerMethodField to either return the field value or None if the field doesn't exist, or you could not use serializers at all and simply write a view that returns the response directly.
Update for REST framework 3.0 serializer.fields can be modified on an instantiated serializer. When dynamic serializer classes are required I'd probably suggest altering the fields in a custom Serializer.__init__() method.
The serializers Charfield method has a property allow_blank
By default it is set to False.
Setting it to True will allow you to mark the field as optional during "serialization".
This is the code that you should write
classification = serializers.CharField(source="Classification", allow_blank=True)
Note: required property is used for deserialization.
DynamicSerializer for DRF 3, which allows dynamicly specifying which fields will be used in serializer, which will be excluded, and optionally which will become required!
Create Mixin
class DynamicSerializerMixin:
"""
A Serializer that takes an additional `fields` argument that
controls which fields should be used.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop("fields", None)
excluded_fields = kwargs.pop("excluded_fields", None)
required_fields = kwargs.pop("required_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)
if isinstance(fields, dict):
for field, config in fields.items():
set_attrs(self.fields[field], config)
if excluded_fields is not None:
# Drop any fields that are not specified in the `fields` argument.
for field_name in excluded_fields:
self.fields.pop(field_name)
if required_fields is not None:
for field_name in required_fields:
self.fields[field_name].required = True
Initialize/adjust your serializer by adding DynamicSerializerMixin to inheritence
class UserProfileSerializer(DynamicSerializerMixin, serializers.ModelSerializer):
class Meta:
model = User
fields = (
"id",
'first_name', 'last_name'
"email",
"is_staff",
)
Use it :)
class RoleInvitationSerializer(serializers.ModelSerializer):
invited_by = UserProfileSerializer(fields=['id', 'first_name', 'last_name'])
or in action apis
#action(detail=True, serializer_class=YourSerialzierClass)
def teams_roles(self, request, pk=None):
user = self.get_object()
queryset = user.roles.all()
serializer = self.get_serializer(queryset, many=True, excluded_fields=['user'])
return Response(data=serializer.data)
For this purpose the serializers have the partial argument. If when the serializer is initialized you can pass partial=True. If you are using generics or mixins you can overrider the get_serializer function as follows:
def get_serializer(self, *args, **kwargs):
kwargs['partial'] = True
return super(YOUR_CLASS, self).get_serializer(*args, **kwargs)
And that will do the trick.
Note: This allows all fields to be optional and not only a specific one. If you want only specifics, you can override the method (i.e. update) and add validations of existence for various fields.
What has worked well for me is to set the serializer like so:
classification = serializers.CharField(max_length=20, allow_blank=True, default=None)
From the "it's a terrible hack relying on specific implementation details of both DRF and Django, but it works (at least for now)" files, here's the approach I used to include some additional debugging data in the response from a "create" method implementation on a serializer:
def create(self, validated_data)
# Actual model instance creation happens here...
self.fields["debug_info"] = serializers.DictField(read_only=True)
my_model.debug_info = extra_data
return my_model
This is a temporary approach that lets me use the browsable API to display some of the raw response data received from a particular remote service during the creation process. In the future, I'm inclined to keep this capability, but hide it behind a "report debugging info" flag in the creation request rather than returning the lower level info by default.

Categories

Resources