How to make custom field in DRF Serializer? - python

I have model Transasction
class Transaction(models.Model):
created_at = models.DateTimeField()
and other models that have OneToOneField with Transasction.
class RefillTransactionData(models.Model):
transaction = models.OneToOneField(Transaction, on_delete=models.CASCADE)
class PurchaseTransactionData(models.Model):
transaction = models.OneToOneField(Transaction, on_delete=models.CASCADE)
How can I create a Serializer with a custom field "data" which will contain other serializers?
A Json shema should be like this
{
created_at: "2020-10-01"
data: {
RefillTransactionData: {},
PurchaseTransactionData: {}
}
}
For GET request I can do this with the to_representation method, but I need the same things for all request types.

from rest_framework.fields import Field, SerializerMethodField
class DictField(Field):
"""
A field handle dict fields
"""
type_name = 'dictfield'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.style = {'base_template': 'textarea.html'}
def to_representation(self, value):
"""" custom"""
# do the customization according to u
if isinstance(value, dict) or value is None:
return value
def to_internal_value(self, value):
""" """
# you can check and validate the data fromat
if value == '' or value is None:
return value
pass
def validate_empty_values(self, data):
if data == '':
self.fail('required')
return super().validate_empty_values(data)

Related

Passing partial=True down to nested serializer in DRF

I have two serializers organised like this:
class OuterSerializer():
inner_obj = InnerSerializer(many=True, required=False)
other fields ......
class InnerSerializer():
field_1 = CharField()
field_2 = CharField()
Now my use case is to partial update the outer serializer's model. How I'm doing that is:
def partial_update(self, request, *args, **kwargs):
serializer = OuterSerializer(data=request.data, context={'request': self.request}, partial=True)
serializer.is_valid(raise_exception=True)
data = serializer.data
outerobj = self.service_layer.update(kwargs['pk'], data, request.user)
response_serializer = OpportunitySerializer(instance=outerobj, context={'request': self.request})
return Response(response_serializer.data, HTTPStatus.OK)
The issue is this partial flag does not get passed down to the InnerSerializer.
For example if my request body looks like below, I want it to work:
{"inner_obj":
{
"field_1" : "abc"
}
}
Currently I get a 400 error for this saying the field is required.
What I've tried :
Setting the partial variable within the OuterSerializer in the init method by modifying it as such
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# We pass the "current serializer" context to the "nested one"
self.fields['inner_obj'].context.update(self.context)
self.fields['inner_obj'].partial = kwargs.get('partial')
However this doesn't travel down.
Try to modify the InnerSerializer so that it could accept the partial argument and pass it to its parent, like following:
class InnerSerializer(serializers.Serializer):
field_1 = CharField()
field_2 = CharField()
def __init__(self, *args, **kwargs):
self.partial = kwargs.pop('partial', False)
super().__init__(*args, **kwargs)
class OuterSerializer(serializers.Serializer):
inner_obj = InnerSerializer(many=True, required=False)
other fields ......
def __init__(self, *args, **kwargs):
partial = kwargs.get('partial')
super().__init__(*args, **kwargs)
self.fields['inner_obj'].child.partial = partial
Another possible solution.
You can also override the to_internal_value() method in the InnerSerializer to make it accept partial updates so:
class InnerSerializer(serializers.Serializer):
field_1 = CharField()
field_2 = CharField()
def to_internal_value(self, data):
if self.partial:
return {field: data.get(field, getattr(self.instance, field)) for field in data}
return super().to_internal_value(data)
class OuterSerializer(serializers.Serializer):
inner_obj = InnerSerializer(many=True, required=False)
other fields ......
Edit:
For the error:
KeyError: "Got KeyError when attempting to get a value for field field_2on serializerInnerSerializer`.
The error message you're encountering suggests that the serializer is trying to access the value for field_2 from the data, but it's not present.
Currently to solve the error, you should override the to_representation() method in the InnerSerializer to only include the fields that are present so:
class InnerSerializer(serializers.Serializer):
field_1 = CharField()
field_2 = CharField()
def to_representation(self, instance):
data = super().to_representation(instance)
return {field: value for field, value in data.items() if value is not None}

DRF UniqueTogetherValidator and related model attribute naming problem

I'm trying to configure a UniqueTogetherValidator to create instances from an API in DRF since if I don't, Django throws a 500 when non-unique data is submitted.
The problem is that in my TagSerializer I'm not using the project but using project.slug as a source that I rename to project:
class TagSerializer(ModelSerializer):
project = serializers.CharField(source="project.slug")
class Meta:
model = Tag
fields = [
"id",
"description",
"name",
"project",
]
validators = [
UniqueTogetherValidator(
queryset=Tag.objects.all(),
fields=["project", "name"],
)
]
Here are the models
class Tag(Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
description = models.TextField()
name = models.CharField(max_length=100)
project = models.ForeignKey(Project, on_delete=models.CASCADE)
class Meta:
unique_together = [["name", "project"]]
class Project(Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField("Project name", max_length=50, unique=True)
slug = AutoSlugField(
"Project slug", populate_from="name", always_update=False, unique=True
)
I would like to use the UniqueTogetherValidator, but it throws 'This field is required.' for the project field since it's looking for the source when checking if the fields are required. The payload that I want to submit would be:
{
"name": "myname",
"description": "some-description",
"project": "test"
}
I went with a custom validator starting from the UniqeTogetherValidator and adding a nested_get and nested_getattr for fetching the values of the nested objects. Here is the validator:
from typing import Any, Dict
from functools import reduce
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
from rest_framework.utils.representation import smart_repr
from rest_framework.validators import qs_exists, qs_filter
def nested_get(dictionary: Dict, keys: str, default=None) -> Any:
"""
Apply get to a nested dict given a hierarchical key separated with '.'
"""
return reduce(
lambda d, key: d.get(key, default) if isinstance(d, dict) else default,
keys.split("."),
dictionary,
)
def nested_getattr(instance: Any, attrs: str) -> Any:
"""
Fetch an attribute value from a nested sintance given a hierarchical attrs separated with '.'
"""
return reduce(getattr, [instance] + attrs.split("."))
class UniqueTogetherRelatedValidator:
"""
Validator that corresponds to `unique_together = (...)` on a model class.
Should be applied to the serializer class, not to an individual field.
"""
message = _("The fields {field_names} must make a unique set.")
missing_message = _("This field is required.")
requires_context = True
def __init__(self, queryset, fields, message=None):
self.queryset = queryset
self.fields = fields
self.message = message or self.message
def enforce_required_fields(self, attrs, serializer):
"""
The `UniqueTogetherValidator` always forces an implied 'required'
state on the fields it applies to.
"""
if serializer.instance is not None:
return
missing_items = {
field_name: self.missing_message
for field_name in self.fields
if serializer.fields[field_name].source not in attrs
}
if missing_items:
raise ValidationError(missing_items, code="required")
def filter_queryset(self, attrs, queryset, serializer):
"""
Filter the queryset to all instances matching the given attributes.
"""
# field names => field sources
sources = [serializer.fields[field_name].source for field_name in self.fields]
# If this is an update, then any unprovided field should
# have it's value set based on the existing instance attribute.
if serializer.instance is not None:
for source in sources:
if source not in attrs:
attrs[source.replace(".", "__")] = nested_getattr(
serializer.instance, source
)
# Determine the filter keyword arguments and filter the queryset.
filter_kwargs = {
source.replace(".", "__"): nested_get(attrs, source) for source in sources
}
return qs_filter(queryset, **filter_kwargs)
#staticmethod
def exclude_current_instance(queryset, instance):
"""
If an instance is being updated, then do not include
that instance itself as a uniqueness conflict.
"""
if instance is not None:
return queryset.exclude(pk=instance.pk)
return queryset
def __call__(self, attrs, serializer):
# self.enforce_required_fields(attrs, serializer)
queryset = self.queryset
queryset = self.filter_queryset(attrs, queryset, serializer)
queryset = self.exclude_current_instance(queryset, serializer.instance)
# Ignore validation if any field is None
checked_values = [
value for field, value in attrs.items() if field in self.fields
]
logger.debug(f"{checked_values=}")
if None not in checked_values and qs_exists(queryset):
field_names = ", ".join(self.fields)
message = self.message.format(field_names=field_names)
raise ValidationError(message, code="unique")
def __repr__(self):
return "<%s(queryset=%s, fields=%s)>" % (
self.__class__.__name__,
smart_repr(self.queryset),
smart_repr(self.fields),
)
In my serializer I use it though the Meta class as in the original question.

DRF How to serialize models inheritance ? (read/write)

I have some models
class RootModel(models.Model):
# Some fields
class ElementModel(models.Model):
root = models.ForeignKey(RootModel, related_name='elements', on_delete=models.CASCADE)
class TextModel(ElementModel):
text = models.TextField()
class BooleanModel(ElementModel):
value = models.BooleanField()
a viewset
class RootViewSet(viewsets.ModelViewSet):
queryset = RootModel.objects.all()
serializer_class = RootSerializer
and serializers
class TextSerializer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
class Meta:
model = TextModel
fields = '__all__'
def get_type(self, obj):
return 'TEXT'
class BooleanSerializer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
class Meta:
model = BooleanModel
fields = '__all__'
def get_type(self, obj):
return 'BOOL'
class RootSerializer(WritableNestedModelSerializer):
elements = ...
class Meta:
model = RootModel
fields = '__all__'
WritableNestedModelSerializer comes from drf_writable_nested extension.
I want to GET/POST/PUT a root containing all data
example with GET (same data for POST/PUT)
{
elements: [
{
type: "TEXT",
text: "my awesome text"
},
{
type: "BOOL",
value: true
}
],
...
root fields
...
}
What is the best way for elements field in RootSerializer ?
I also want to have information with OPTIONS method, how can I have it ?
Thanks
Finally I found a solution.
First we need a PolymorphicSerializer class :
from enum import Enum
from rest_framework import serializers
class PolymorphicSerializer(serializers.Serializer):
"""
Serializer to handle multiple subclasses of another class
- For serialized dict representations, a 'type' key with the class name as
the value is expected: ex. {'type': 'Decimal', ... }
- This type information is used in tandem with get_serializer_map(...) to
manage serializers for multiple subclasses
"""
def get_serializer_map(self):
"""
Return a dict to map class names to their respective serializer classes
To be implemented by all PolymorphicSerializer subclasses
"""
raise NotImplementedError
def to_representation(self, obj):
"""
Translate object to internal data representation
Override to allow polymorphism
"""
if hasattr(obj, 'get_type'):
type_str = obj.get_type()
if isinstance(type_str, Enum):
type_str = type_str.value
else:
type_str = obj.__class__.__name__
try:
serializer = self.get_serializer_map()[type_str]
except KeyError:
raise ValueError('Serializer for "{}" does not exist'.format(type_str), )
data = serializer(obj, context=self.context).to_representation(obj)
data['type'] = type_str
return data
def to_internal_value(self, data):
"""
Validate data and initialize primitive types
Override to allow polymorphism
"""
try:
type_str = data['type']
except KeyError:
raise serializers.ValidationError({
'type': 'This field is required',
})
try:
serializer = self.get_serializer_map()[type_str]
except KeyError:
raise serializers.ValidationError({
'type': 'Serializer for "{}" does not exist'.format(type_str),
})
validated_data = serializer(context=self.context).to_internal_value(data)
validated_data['type'] = type_str
return validated_data
def create(self, validated_data):
"""
Translate validated data representation to object
Override to allow polymorphism
"""
serializer = self.get_serializer_map()[validated_data['type']]
validated_data.pop('type')
return serializer(context=self.context).create(validated_data)
def update(self, instance, validated_data):
serializer = self.get_serializer_map()[validated_data['type']]
validated_data.pop('type')
return serializer(context=self.context).update(instance, validated_data)
and now :
class ElementSerializer(PolymorphicSerializer):
class Meta:
model = ElementModel
def get_serializer_map(self):
return {
BooleanSerializer.__class__: BooleanSerializer,
TextSerializer.__class__: TextSerializer,
}
class RootSerializer(WritableNestedModelSerializer):
elements = ElementSerializer(many=True)
class Meta:
model = RootModel
fields = '__all__'
Reference link: https://stackoverflow.com/a/44727343/5367584

Django Rest Framework custom ListSerializer only returning dictionary keys, not values

I am using a custom ListSerializer to reformat a JSON response and it generates the proper format when I log from inside the serializer, but when it is returned to the ViewSet, the response becomes a list of the dictionary keys rather than including the entire dictionary with both keys and values.
I have made a simplified example of the problem with hard-coded JSON to illustrate what I believe to be the crux of the issue.
views.py
class ElementViewSet(viewsets.ViewSet):
...
#detail_route(methods=['get'])
def elements(self, request, pk=None):
elements = ElementFilterSet(params)
serializer = serializers.ElementSerializer(elements, many=True)
return Response(serializer.data)
serializers.py
class ElementListSerializer(serializers.ListSerializer):
def to_representation(self, obj):
result = {"home": {"label_color": "#123456","label_text": "young"},"speak": { "label_color": "","label_text": "Hello"}}
return result
class ElementSerializer(serializers.ModelSerializer):
class Meta:
model = Element
list_serializer_class = ElementListSerializer
def to_representation(self, obj):
result = super(ElementSerializer, self).to_representation(obj)
return result
The response I get is a list of dictionary keys:
[
"speak",
"home"
]
rather than what I want, which is the entire dictionary (in this case, simply the hard-coded JSON result):
{
"home": {
"label_color": "#123456",
"label_text": "young"
},
"speak": {
"label_color": "",
"label_text": "Hello"
}
}
I am new to customizing serializers and using list_serializer_class; I likely simply don't understand how they work, but this behavior does seem unexpected to me.
According to docs definition: The ListSerializer class provides the behavior for serializing and validating multiple objects at once.
You don't typically need ListSerializers, if the data you pass could be represented as serializer data.
Suggested solution is to use nested serializers, if you dont get the key values from a model instance:
class ElementListSerializer(serializers.BaseSerializer):
def to_representation(self, obj):
return {
'home': {"label_color": "#123456",
"label_text": "young"},
'speak': {
"label_color": "",
"label_text": "Hello"}
}
class ElementSerializer(serializers.ModelSerializer):
element_list = ElementListSerializer()
class Meta:
model = Element
def create(self, validated_data):
data = validated_data.pop('element_list')
return data
The problem is in property data on ListSerializer which returns ReturnList instead of ReturnDict.
To fix your code, you have to change data property:
from rest_framework import serializers
class ElementListSerializer(serializers.ListSerializer):
def to_representation(self, obj):
result = {"home": {"label_color": "#123456","label_text": "young"},"speak": { "label_color": "","label_text": "Hello"}}
return result
#property
def data(self):
ret = serializers.BaseSerializer.data.fget(self)
return serializers.ReturnDict(ret, serializer=self)
class ElementSerializer(serializers.ModelSerializer):
class Meta:
model = Element
list_serializer_class = ElementListSerializer
def to_representation(self, obj):
result = super(ElementSerializer, self).to_representation(obj)
return result
You can also create a more generic solution. It will automatically convert list of dicts with same structure to one dict where keys will be from specified field from child's dict.
class ListToDictSerializer(serializers.ListSerializer):
def to_representation(self, data):
return {
item[self.child.Meta.dict_serializer_key]: self.child.to_representation(item)
for item in data
}
#property
def data(self):
ret = drf_serializers.BaseSerializer.data.fget(self)
return serializers.ReturnDict(ret, serializer=self)
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = Model
list_serializer_class = ListToDictSerializer
dict_serializer_key = 'id'

django-rest-framework + django-polymorphic ModelSerialization

I was wondering if anyone had a Pythonic solution of combining Django REST framework with django-polymorphic.
Given:
class GalleryItem(PolymorphicModel):
gallery_item_field = models.CharField()
class Photo(GalleryItem):
custom_photo_field = models.CharField()
class Video(GalleryItem):
custom_image_field = models.CharField()
If I want a list of all GalleryItems in django-rest-framework it would only give me the fields of GalleryItem (the parent model), hence: id, gallery_item_field, and polymorphic_ctype. That's not what I want. I want the custom_photo_field if it's a Photo instance and custom_image_field if it's a Video.
So far I only tested this for GET request, and this works:
class PhotoSerializer(serializers.ModelSerializer):
class Meta:
model = models.Photo
class VideoSerializer(serializers.ModelSerializer):
class Meta:
model = models.Video
class GalleryItemModuleSerializer(serializers.ModelSerializer):
class Meta:
model = models.GalleryItem
def to_representation(self, obj):
"""
Because GalleryItem is Polymorphic
"""
if isinstance(obj, models.Photo):
return PhotoSerializer(obj, context=self.context).to_representation(obj)
elif isinstance(obj, models.Video):
return VideoSerializer(obj, context=self.context).to_representation(obj)
return super(GalleryItemModuleSerializer, self).to_representation(obj)
For POST and PUT requests you might want to do something similiar as overriding the to_representation definition with the to_internal_value def.
Here's a general and reusable solution. It's for a generic Serializer but it wouldn't be difficult to modify it to use ModelSerializer. It also doesn't handle serializing the parent class (in my case I use the parent class more as an interface).
from typing import Dict, Type
from rest_framework import serializers
class PolymorphicSerializer(serializers.Serializer):
"""
Serializer to handle multiple subclasses of another class
- For serialized dict representations, a 'type' key with the class name as
the value is expected: ex. {'type': 'Decimal', ... }
- This type information is used in tandem with get_serializer_map(...) to
manage serializers for multiple subclasses
"""
def get_serializer_map(self) -> Dict[str, Type[serializers.Serializer]]:
"""
Return a dict to map class names to their respective serializer classes
To be implemented by all PolymorphicSerializer subclasses
"""
raise NotImplementedError
def to_representation(self, obj):
"""
Translate object to internal data representation
Override to allow polymorphism
"""
type_str = obj.__class__.__name__
try:
serializer = self.get_serializer_map()[type_str]
except KeyError:
raise ValueError(
'Serializer for "{}" does not exist'.format(type_str),
)
data = serializer(obj, context=self.context).to_representation(obj)
data['type'] = type_str
return data
def to_internal_value(self, data):
"""
Validate data and initialize primitive types
Override to allow polymorphism
"""
try:
type_str = data['type']
except KeyError:
raise serializers.ValidationError({
'type': 'This field is required',
})
try:
serializer = self.get_serializer_map()[type_str]
except KeyError:
raise serializers.ValidationError({
'type': 'Serializer for "{}" does not exist'.format(type_str),
})
validated_data = serializer(context=self.context) \
.to_internal_value(data)
validated_data['type'] = type_str
return validated_data
def create(self, validated_data):
"""
Translate validated data representation to object
Override to allow polymorphism
"""
serializer = self.get_serializer_map()[validated_data['type']]
return serializer(context=self.context).create(validated_data)
And to use it:
class ParentClassSerializer(PolymorphicSerializer):
"""
Serializer for ParentClass objects
"""
def get_serializer_map(self) -> Dict[str, Type[serializers.Serializer]]:
"""
Return serializer map
"""
return {
ChildClass1.__name__: ChildClass1Serializer,
ChildClass2.__name__: ChildClass2Serializer,
}
For sake of completion, I'm adding to_internal_value() implementation, since I needed this in my recent project.
How to determine the type
Its handy to have possibility to distinguish between different "classes"; So I've added the type property into the base polymorphic model for this purpose:
class GalleryItem(PolymorphicModel):
gallery_item_field = models.CharField()
#property
def type(self):
return self.__class__.__name__
This allows to call the type as "field" and "read only field".
type will contain python class name.
Adding type to Serializer
You can add the type into "fields" and "read only fields"
(you need to specify type field in all the Serializers though if you want to use them in all Child models)
class PhotoSerializer(serializers.ModelSerializer):
class Meta:
model = models.Photo
fields = ( ..., 'type', )
read_only_fields = ( ..., 'type', )
class VideoSerializer(serializers.ModelSerializer):
class Meta:
model = models.Video
fields = ( ..., 'type', )
read_only_fields = ( ..., 'type', )
class GalleryItemModuleSerializer(serializers.ModelSerializer):
class Meta:
model = models.GalleryItem
fields = ( ..., 'type', )
read_only_fields = ( ..., 'type', )
def to_representation(self, obj):
pass # see the other comment
def to_internal_value(self, data):
"""
Because GalleryItem is Polymorphic
"""
if data.get('type') == "Photo":
self.Meta.model = models.Photo
return PhotoSerializer(context=self.context).to_internal_value(data)
elif data.get('type') == "Video":
self.Meta.model = models.Video
return VideoSerializer(context=self.context).to_internal_value(data)
self.Meta.model = models.GalleryItem
return super(GalleryItemModuleSerializer, self).to_internal_value(data)

Categories

Resources