How to use DRF Custom serializer field with DB model - python

I'm using relation database which is having Binary Field, So how can I use DRF serializer to save the field value
I have referred the documentation https://www.django-rest-framework.org/api-guide/fields/#custom-fields
and understood some of the part and created below, but I'm not sure how to use it in serializer
Model
class MyData(models.Model):
data = models.BinaryField()
Custom Field
class BinaryField(serializers.Field):
def to_representation(self, value):
return value.decode('utf-8')
def to_internal_value(self, value):
return value.encode('utf-8')
But how should I use this in my below serializer
class BlobDataSerializer (serializers.ModelSerializer):
class Meta:
model = MyData
fields = ('id', 'data')
So basically I'm trying to store incoming data in binary field. Thanks in advance

Like this:
class BlobDataSerializer (serializers.ModelSerializer):
class Meta:
model = MyData
fields = ('id', 'data')
data = BinaryField()
For a more reusable solution, you could also subclass ModelSerializer and customize the serializer_field_mapping.
See https://www.django-rest-framework.org/api-guide/serializers/#customizing-field-mappings

What about using Serializer Method Field
class BlobDataSerializer(serializers.ModelSerializer):
data = serializers.SerializerMethodField()
def get_data(self, obj):
return obj.decode('utf-8')
class Meta:
model = MyData
fields = ('id', 'data')

Explicitly per field
In the serializer, specify the field type for each custom field.
class BlobDataSerializer (serializers.ModelSerializer):
class Meta:
model = MyData
fields = ('id', 'data')
data = BinaryField()
Implicitly for all fields
For a more reusable solution, you could also subclass ModelSerializer and customize the serializer_field_mapping.
Because we need to override a class variable (not an instance variable), it's not as straightforward as the above solution and it's also kind of "magical".
class BlobDataSerializerMetaClass(type(serializers.ModelSerializer)):
def __new__(cls, clsname, bases, attrs):
# Call the __new__ method from the ModelSerializer metaclass
super_new = super().__new__(cls, clsname, bases, attrs)
# Modify class variable "serializer_field_mapping"
# serializer_field_mapping: model field -> serializer field
super_new.serializer_field_mapping[models.BinaryField] = BinaryField
return super_new
# Set the above metaclass as the serializer metaclass
class BlobDataSerializer (serializers.ModelSerializer, metaclass=BlobDataSerializerMetaClass):
class Meta:
model = MyData
fields = ('id', 'data')
See https://www.django-rest-framework.org/api-guide/serializers/#customizing-field-mappings

Related

Django pass field from serializer to model.save() that is not present in the model

I need to pass fields that are present in serializer, but not present in model to model save method (I have complicated saving logic and I want to make some decisions in object creation based on these fields). How can I do that? I tried to add
non_db_field = property to model, but I still get error MyModel() got an unexpected keyword argument 'negative_amount'
Let's say my model is
class MyModel(AbstractModel):
field1 = models.DateTimeField()
field2 = models.BigIntegerField()
My serializer is
class MyModelSerializer(AbstractSerializer):
field3 = serializers.BooleanField(required=False)
class Meta(AbstractSerializer.Meta):
model = MyModel
fields = '__all__'
And my viewset is
class MyModelViewSet(AbstractViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
You should handle this behavior in serializer.save method, for example, you can pop it from validated_data like that:
def save(self, **kwargs):
self.validated_data.pop("negative_amount")
return super().save(**kwargs)
You can use fields=['field1', 'field2', 'field3'] in serializer instead of fields='__all__'.
I found a solution based partly on Sharpek's answer and partly based on this answer:
In serializer I override save method:
def save(self, **kwargs):
if 'field3' in self.validated_data:
kwargs['field3'] = self.validated_data.pop('field3')
return super().save(**kwargs)
In models I override init method and define field:
field3 = None
def __init__(self, *args, **kwargs):
if 'field3' in kwargs:
self.field3 = kwargs.pop('field3')
super(Reading, self).__init__(*args, **kwargs)

How to add new serializer field along with the all model fields?

Here I have a model which has so many fields. So I want to use __all__ to return all the fields. But now I needed to add new field image_url so I customize a serializer like this but now with this I need to put all the model fields in the Meta class like this fields=['name','..', 'image_url'] in order to return the image_url.
Is there any way to return image_url without specifying it in the Meta.fields ?
I mean I don't want to write all the model fields in the Meta.fields (since the fields are too many) and want to return the image_url also.
serializers.py
class MySerializer(ModelSerializer):
image_url = serializers.SerializerMethodField('get_image_url')
class Meta:
model = MyModel
fields = '__all__'
def get_image_url(self, obj):
return obj.image.url
You can try to subclass te serializer:
class MySerializer(ModelSerializer):
class Meta:
model = MyModel
fields = '__all__'
class MyChildSerializer(MySerializer):
image_url = serializers.SerializerMethodField()
class Meta:
fields = MySerializer.Meta.fields + ['image_url']
def get_image_url(self, obj):
return obj.image.url
Never tried something like this, but since Meta.fields is a list you can perform basic python operations on it.
ps. If you're using pattern get_<field_name> for getter, you do not need to specify it in SerializerMethodField arguments.
Try this:
class MySerializer(ModelSerializer):
image_url = serializers.SerializerMethodField()
class Meta:
model = MyModel
fields = [f.name for f in MyModel._meta.fields] + ['image_url']
def get_image_url(self, obj):
return obj.image.url

DRF SerializerMethodField how to pass parameters

Is there a way to pass paremeters to a Django Rest Framework's SerializerMethodField?
Assume I have the models:
class Owner(models.Model):
name = models.CharField(max_length=10)
class Item(models.Model):
name = models.CharField(max_length=10)
owner = models.ForeignKey('Owner', related_name='items')
itemType = models.CharField(max_length=5) # either "type1" or "type2"
What I need is to return an Owner JSON object with the fields: name, type1items, type2items.
My current solution is this:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = models.Item
fields = ('name', 'itemType')
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getType1Items')
type2items = serializers.SerializerMethodField(method_name='getType2Items')
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getType1Items(self, ownerObj):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType="type1")
return ItemSerializer(queryset, many=True).data
def getType2Items(self, ownerObj):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType="type2")
return ItemSerializer(queryset, many=True).data
This works. But it would be much cleaner if I could pass a parameter to the method instead of using two methods with almost the exact code. Ideally it would look like this:
...
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getItems', "type1")
type2items = serializers.SerializerMethodField(method_name='getItems', "type2")
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getItems(self, ownerObj, itemType):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType=itemType)
return ItemSerializer(queryset, many=True).data
In the docs SerializerMethodField accepts only one parameter which is method_name.
Is there any way to achieve this behaviour using SerializerMethodField? (The example code here is overly simplified so there might be mistakes.)
There is no way to do this with the base field.
You need to write a custom serializer field to support it. Here is an example one, which you'll probably want to modify depending on how you use it.
This version uses the kwargs from the field to pass as args to the function. I'd recommend doing this rather than using *args since you'll get more sensible errors, and flexibility in how you write your function/field definitions.
class MethodField(SerializerMethodField):
def __init__(self, method_name=None, **kwargs):
# use kwargs for our function instead, not the base class
super().__init__(method_name)
self.func_kwargs = kwargs
def to_representation(self, value):
method = getattr(self.parent, self.method_name)
return method(value, **self.func_kwargs)
Using the field in a serializer:
class Simple(Serializer):
field = MethodField("get_val", name="sam")
def get_val(self, obj, name=""):
return "my name is " + name
>>> print(Simple(instance=object()).data)
{'field': 'my name is sam'}
You could just refactor what you have:
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getType1Items')
type2items = serializers.SerializerMethodField(method_name='getType2Items')
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getType1Items(self, ownerObj):
return getItems(ownerObj,"type1")
def getType2Items(self, ownerObj):
return getItems(ownerObj,"type2")
def getItems(self, ownerObj, itemType):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType=itemType)
return ItemSerializer(queryset, many=True).data

How to make a field editable=False in DRF

I've a serializer. I want to restrict updating a field. How would I do that?
class ABCSerializer(serializers.ModelSerializer):
class Meta:
"""Meta."""
model = ModelA
fields = ('colA', 'colB', 'colC',)
colA is a required field while creating the object. However, it should not be allowed to update. How can I do that??
Sounds like you need different serializers for PUT and POST methods. In the serializer for the PUT method you can set the colA field to readonly
class ABCViewSet(ModelViewSet):
serializer_class = ABCSerializer
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method == 'PUT':
serializer_class = SerializerWithReadOnlyColA
return serializer_class
You can use Django REST Frameworks field-level validation by validating that field has not changed on update like so:
from rest_framework.exceptions import ValidationError
class ABCSerializer(serializers.ModelSerializer):
colA = serializers.CharField(max_length=100)
def validate_colA(self, value):
if self.instance and self.instance.colA != value:
raise ValidationError("You may not edit colA")
return value
class Meta:
"""Meta."""
model = ModelA
fields = ('colA', 'colB', 'colC',)
This will check whether or not this is an update (via checking if an instance is populated on the serializer) and if so it will then check to see if you have made a change to the field and if you have it will throw a ValidationError. The benefit of this approach is that you can keep your view code the same as before and continue to keep your validation behaviour in your serializer.
You can override the serializer's update method to only update fields that you want.
class ABCSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
instance.colB = validated_data.get('colB', instance.colB)
instance.colC = validated_data.get('colC', instance.colC)
# do nothing to instance.colA
instance.save()
return instance
class Meta:
model = ModelA
fields = ('colA', 'colB', 'colC',)
Or if you have many fields, and just want to omit updating colA, you could write your update method like this:
def update(self, instance, validated_data):
validated_data.pop('colA') # validated_data no longer has colA
return super().update(instance, validated_data)
You can read more about overriding update here: https://www.django-rest-framework.org/api-guide/serializers/#saving-instances
I think it's too late to answer but this may be useful for others:)
you can solve your problem this way:
class ABCSerializer(serializers.ModelSerializer):
class Meta:
model = ModelA
fields = ('colA', 'colB', 'colC',)
def get_fields(self):
fields = super().get_fields()
if self.instance:
fields["colA"].read_only = True
return fields
When you want to create, the self.instance is None, it will pass the if clause, and in case of updating the if clause will make the field read only and non-editable.
You can do this with the read_only_fieldsoption
class ABCSerializer(serializers.ModelSerializer):
class Meta:
"""Meta."""
model = ModelA
fields = ('colB', 'colC',)
read_only_fields = ('colA',)

'Dynamic' fields in DRF serializers

My aim is to build endpoint which will surve to create objects of model with GenericForeignKey. Since model also includes ContentType, the actual type of model which we will reference is not known before object creation.
I will provide an example:
I have a 'Like' model which can reference a set of other models like 'Book', 'Author'.
class Like(models.Model):
created = models.DateTimeField()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
Serializer may look like this:
class LikeSerializer(serializers.ModelSerializer):
class Meta:
model = models.Like
fields = ('id', 'created', )
What I want to achieve is to determine type of Like based on keys passed in request. The problem is that DRF do not pass those keys from request if they were not expilictly specified in Serializer fields. For example, POST request body contains:
{
"book":2
}
I want to do next
def restore_object(self, attrs, instance=None)
if attrs.get('book', None) is not None:
# create Like instance with Book contenttype
elif attrs.get('author', None) is not None:
# create Like instance with Author contenttype
In this case first if clause will be executed.
As you can see, The type determined based on key passed in request, without specifying special Field.
Is there any way to achieve this?
Thanks
You might try instantiating your serializer whenever your view is called by wrapping it in a function (you make a serializer factory):
def like_serializer_factory(type_of_like):
if type_of_like == 'book':
class LikeSerializer(serializers.ModelSerializer):
class Meta:
model = models.Like
fields = ('id', 'created', )
def restore_object(self, attrs, instance=None):
# create Like instance with Book contenttype
elif type_of_like == 'author':
class LikeSerializer(serializers.ModelSerializer):
class Meta:
model = models.Like
fields = ('id', 'created', )
def restore_object(self, attrs, instance=None):
# create Like instance with Author contenttype
return LikeSerializer
Then override this method in your view:
def get_serializer_class(self):
return like_serializer_factory(type_of_like)
Solution 1
Basically there is a method you can add on GenericAPIView class called get_context_serializer
By default your view, request and format class are passed to your serializer
DRF code for get_context_serializer
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
you can override that on your view like this
def get_serializer_context(self):
data = super().get_serializer_context()
# Get the book from post and add to context
data['book'] = self.request.POST.get('book')
return data
And use this on your serializer class
def restore_object(self, attrs, instance=None):
# Get book from context to use
book = self.context.get('book', None)
author = attrs.get('author', None)
if book is not None:
# create Like instance with Book contenttype
pass
elif author is not None:
# create Like instance with Author contenttype
pass
Solution 2
Add a field on your serializer
class LikeSerializer(serializers.ModelSerializer):
# New field and should be write only, else it will be
# return as a serializer data
book = serializers.IntegerField(write_only=True)
class Meta:
model = models.Like
fields = ('id', 'created', )
def save(self, **kwargs):
# Remove book from validated data, so the serializer does
# not try to save it
self.validated_data.pop('book', None)
# Call model serializer save method
return super().save(**kwargs)

Categories

Resources