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.
Related
I'm trying to access the dictonary inside the jsonfield serializer "assigned_facilities". But i'm receiving the following error:
django.db.utils.IntegrityError: null value in column "assigned_facilities_id" of relation "users_leadfacilityassign" violates not-null constraint
DETAIL: Failing row contains (78, null, null, 159).
File "/app/users/api/views.py", line 53, in perform_create
serializer.save(agent=self.request.user)
File "/usr/local/lib/python3.9/site-packages/rest_framework/serializers.py", line 205, in save
self.instance = self.create(validated_data)
File "/app/users/api/serializers.py", line 252, in create
instance.leadfacility.create(assigned_facilities_id=assigned_facilities.get('facility_id'), datetime=assigned_facilities.get('datetime'))
I'm basically trying to create a "LeadFacilityAssign" object for each item inside my json so i can have a "LeadFacilityAssign" object for each facility i want to add to a lead.
Does anyone know what is causing this error? I tried a few different things but nothing worked so far.
json
{
"facilities": [{
"facility_id": "1",
"datetime": "2018-12-19 09:26:03.478039"
},
{
"facility_id": "1",
"datetime": "2018-12-19 09:26:03.478039"
}
]
}
serializers.py
class LeadUpdateSerializer(serializers.ModelSerializer):
is_owner = serializers.SerializerMethodField()
assigned_facilities = serializers.JSONField(required=False, allow_null=True, write_only=True)
class Meta:
model = Lead
fields = (
"id",
"first_name",
"last_name",
"is_owner",
"assigned_facilities",
)
read_only_fields = ("id", "is_owner")
def get_is_owner(self, obj):
user = self.context["request"].user
return obj.agent == user
def create(self, validated_data):
assigned_facilities = validated_data.pop("assigned_facilities")
instance = Lead.objects.create(**validated_data)
for facility in assigned_facilities:
instance.leadfacility.create(assigned_facilities_id=assigned_facilities.get('facility_id'), datetime=assigned_facilities.get("datetime"))
return instance
models.py
class Facility(models.Model):
name = models.CharField(max_length=150, null=True, blank=False)
def __str__(self):
return self.name
class Lead(models.Model):
first_name = models.CharField(max_length=40, null=True, blank=True)
last_name = models.CharField(max_length=40, null=True, blank=True)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class LeadFacilityAssign(models.Model):
assigned_facilities = models.ForeignKey(Facility, on_delete=models.CASCADE, related_name='leadfacility')
lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name='leadfacility')
datetime = models.DateTimeField()
views.py
class LeadCreateView(CreateAPIView):
permission_classes = [IsAuthenticated, IsLeadOwner]
serializer_class = LeadUpdateSerializer
def perform_create(self, serializer):
serializer.save(agent=self.request.user)
class LeadUpdateView(UpdateAPIView):
permission_classes = [IsAuthenticated, IsLeadOwner]
serializer_class = LeadUpdateSerializer
def get_queryset(self):
return Lead.objects.all()
You have three tables:
Lead table in which lead_id is non nullable since it is primary key
Facility table in which facility_id is non nullable since it is primary key
LeadFacility table in which lead_facility_id is non nullable but its two foreign keys (lead_id and facility_id) are nullable.
And you are assigning these nullable value to your non nullable field.
Maybe you are trying to do :
lead_falility_id (table: LeadFacility talbe) = facility_id (table: Facility)
But by mistake you are doing :
lead_falility_id (table: LeadFacility talbe) = facility_id (table: LeadFacility).
And because of this, your are doing :
lead_facility_id = null for the non nullable field.
Your JSON doesn't match your serializer.
Your serializer fields don't match your model fields.
Your views don't match your models or your serializers.
So, let's take it from the top.
If I understand correctly, you want to create a LeadFacilityAssign object at the same time as creating or updating a Lead object. There are some approaches to solve this, like using a post_save signal right after a Lead create request, but let's follow your drift...
From your Lead serializer, this is "fine":
class LeadUpdateSerializer(serializers.ModelSerializer):
is_owner = serializers.SerializerMethodField()
assigned_facilities = serializers.JSONField(required=False, allow_null=True, write_only=True)
class Meta:
model = Lead
fields = (
"id",
"first_name",
"last_name",
"is_owner",
"assigned_facilities",
)
read_only_fields = ("id", "is_owner")
But this:
def get_is_owner(self, obj):
user = self.context["request"].user
return obj.agent == user
Has a comparison statement (==) in the last line, meaning it could return a True or False value (?), plus you're not using the "agent" field anywhere else, nor is declared in the serializer or even a field in your models. Just get rid of that function or add the "agent" field in your LeadFacilityAssign model (assuming you'll assign a Lead, a Facility and an Agent to that relationship).
What I'm guessing you're expecting from you JSON call is the "facilities" info. From your declared fields above I guess you should be expecting an "assigned_facilities" field, which doesn't show in your JSON data, but let's assume your API will receive an "assigned_facilities" field instead of the "facilities" sub-dict with many facilities related to a single Lead.
I haven't tested the code below, but according to the REST API docs, you have to define two methods in your serializer now, one for CREATE and one for UPDATE.
Create:
def create(self, validated_data):
lead = Lead.objects.create(first_name=validated_data['first_name'], last_name=validated_data['last_name'] #Here you will create the Lead object that you will reference later in your LeadFacilityAssign relationship with the dictionary information from the received data, so let's save it:
lead.save()
#Now we need to create all facilities relationships to this Lead:
facilities = validated_data['assigned_facilities'] #This will create a "facilities" sub-dict from your received data with a facility_id and a datetime field in key-value pair.
for item in facilities:
facility = Facility.objects.get(id=item['facility_id']) #Get a single facility object for each ID in your JSON. If this fails, try converting it to int().
datetime = item['datetime'] #Again, if it fails because it's taken as string, try converting it to datetime object.
entry = LeadFacilityAssign.objects.create(assigned_facilities=facility, lead=lead, datetime=datetime) #Create the entry.
entry.save() #Save the entry
return #Exit your function
The Update method should look more or less the same.
In the view, if you're not using the "agent" field just parse the user it for safety or just use it later if you want to include it as owner in your model.
class LeadCreateView(CreateAPIView):
permission_classes = [IsAuthenticated, IsLeadOwner]
serializer_class = LeadUpdateSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Your JSON sample include "facilities" but your serializer has assigned_facilities = serializers.JSONField(required=False, allow_null=True, write_only=True).
class LeadUpdateSerializer(serializers.ModelSerializer):
is_owner = serializers.SerializerMethodField()
facilities = serializers.JSONField(required=False, allow_null=True, write_only=True)
class Meta:
model = Lead
fields = (
"id",
"first_name",
"last_name",
"PrimaryAddress",
"City",
"PostalCode",
"RegionOrState",
"pc_email",
"Cell",
"secphone",
"client_cell",
"client_secphone",
"birthday",
"curr_client_address",
"curr_client_city",
"curr_client_zip",
"ideal_address",
"ideal_city",
"ideal_zip",
"ideal_state",
"budget",
"client_email",
"client_first_name",
"client_last_name",
"lead_status",
"created_at",
"agent",
"is_owner",
"relationship",
"marital_status",
"gender",
"pets",
"facilities",
)
read_only_fields = ("id", "created_at", "agent", "is_owner")
def get_is_owner(self, obj):
user = self.context["request"].user
return obj.agent == user
def create(self, validated_data):
facilities = validated_data.pop("facilities", None)
instance = Lead.objects.create(**validated_data)
for facilities in assigned_facilities:
LeadFacilityAssign.objects.create(assigned_facilities_id=assigned_facilities.get("facility_id"), datetime=assigned_facilities.get("datetime"), lead=instance)
return instance
Also you added required False for facilities, so validated.pop("facilities") might be caused error if there is no facilities in request. You should add another parameter into pop method. validated.pop("facilities", None)
Your Query will become like this
instance.leadfacility.create(assigned_facilities_id__id=assigned_facilities.get('facility_id'), datetime=assigned_facilities.get("datetime"))
NOTE-
because assigned_facilities_id return full object of foreign key & assigned_facilities_id__id return value of id foreign key object
I created a model(AnalysisFieldTemplate) with a foreign key to AnalysisFieldRule.
What i want is to have the possibility to leave the field display_analysis_field_rule blank and save my admin form.
class AnalysisFieldRule(models.Model):
action_kind: str = models.CharField(
choices=ActionKind.choices,
default=ActionKind.DISPLAY,
max_length=text_choices_max_length(ActionKind),
verbose_name=_("action kind"),
)
formula: str = formula_field()
name: str = models.CharField(max_length=MAX_NAME_LENGTH, verbose_name=_("name"))
def __str__(self):
return f"{self.name}: {ActionKind(self.action_kind).label}"
class Meta:
constraints = [
UniqueConstraint(fields=("action_kind", "name"), name="%(app_label)s_%(class)s_is_unique"),
]
ordering = ["name", "action_kind"]
verbose_name = _("analysis field rule")
verbose_name_plural = _("analysis field rules")
class AnalysisFieldTemplate(models.Model):
display_analysis_field_rule: AnalysisFieldRule = models.ForeignKey(
AnalysisFieldRule,
blank=True,
limit_choices_to={"action_kind": ActionKind.DISPLAY},
null=True,
on_delete=models.PROTECT,
related_name="display_analysis_field_templates",
verbose_name=_("display rule"),
)
Now here is the problem. If i try to save my admin form without choosing one a value for display_analysis_field_rule it will result in an Validationerror.
It seems that the standard empty value for a foreign key "------" is not a valid choice.
#admin.register(AnalysisFormTemplate)
class AnalysisFieldTemplateAdmin(admin.ModelAdmin):
fieldsets = (
(
None,
{
"fields": (
"name",
"name_for_formula",
"ordering",
"required",
"kind",
"display_analysis_field_rule",
"highlight_analysis_field_rule",
)
},
),
(
_("Text options"),
{"fields": ("max_length",)},
),
(
_("Integer options"),
{"fields": ("min_integer_value", "max_integer_value")},
),
(_("Amount of money options"), {"fields": ("min_amount_of_money", "max_amount_of_money")}),
)
I debugged a little deeper and found that the "to_python" compares the choosen value with pythons standard "empty_values" but of course it contains not the "------" and it will handle it as a normal id which results in an Validation error.
My question is how can i make it possible to save my form without choosing a value for my foreign key? Do i have to override the "to_python" function? What would be a best practice here?
I appreciate all the help :)
You need to create custom modelform for your use case and make the particular field with required=False
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.fields['display_analysis_field_rule'].required = False
self.fields['display_analysis_field_rule'].empty_label = None
class Meta:
model = MyModel # Put your model name here
#admin.register(AnalysisFormTemplate)
class AnalysisFieldTemplateAdmin(admin.ModelAdmin):
form = MyForm
# .... your stuff
The solution which worked for me was to create a custom form and override the empy_values for each of my foreing keys with its empty_label.
from django.core.validators import EMPTY_VALUES # NOQA
def empty_values_list_for_foreign_key(empty_label:str):
empty_values_list = list(EMPTY_VALUES)
empty_values_list.append(empty_label)
return empty_values_list
class AnalysisFieldTemplateAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["display_analysis_field_rule"].empty_values = empty_values_list_for_foreign_key(self.fields["display_analysis_field_rule"].empty_label)
self.fields["highlight_analysis_field_rule"].empty_values = empty_values_list_for_foreign_key(self.fields["highlight_analysis_field_rule"].empty_label)
#admin.register(AnalysisFieldTemplate)
class AnalysisFieldTemplateAdmin(admin.ModelAdmin):
form = AnalysisFieldTemplateAdminForm
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
I am building an API backend with django rest framework for angular cli and I have no idea how to access verbose_name's model fields attribute in order to serialize it. This is my code:
models.py
class MyModel(model.Models):
myField = models.CharField(verbose_name='My Verbose Name')
# Here I have other 30 fields
this is the serializer for this model
serializers.py
class MyModelSerializer(ModelSerializer):
myField = SerializerMethodField()
def get_myField(self, obj):
field = next(f for f in obj._meta.fields if f.name == 'myField')
myField= {
'verbose_name': field.verbose_name.title(),
'value': obj.myField
}
return myField
# Do I Have to repeat this function for every field on my Model in order to have verbose-name ??
class Meta:
model = MyModel
fields = ['nominativo' ]
and my view is:
class MyModelListAPIView(ListAPIView):
queryset = Archivio.objects.all()
serializer_class = MyModelSerializer
My output is like i desire and is like this:
[
{
"myField":
{
"verbose_name":"My Verbose Name",
"value":"My Field value"
}
}
]
But this is okay with only few fields. In My case I need to have an output like this with 30 fields of my Model. Is there a better solution instead of making 30 get_field functions on my ModelSerializer class?
I think playing around with the fields itself (changing representation to object) might not be the best way to achieve what you need (surely not the easiest).
Instead, I would try to define my ModelSerializer which autogenerates "labels" field, for example (basic implementation):
from rest_framework import serializers
from rest_framework.fields import SerializerMethodField
class MyModelSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super(MyModelSerializer, self).__init__(*args, **kwargs)
if 'labels' in self.fields:
raise RuntimeError(
'You cant have labels field defined '
'while using MyModelSerializer'
)
self.fields['labels'] = SerializerMethodField()
def get_labels(self, *args):
labels = {}
for field in self.Meta.model._meta.get_fields():
if field.name in self.fields:
labels[field.name] = field.verbose_name
return labels
Then, if you would use MyModelSerializer instead of serializers.ModelSerializer, you would get output like:
{
'name': 'Test',
'email': 'test#test.com',
'labels': {
'name': 'Full name',
'email': 'Email address'
}
}
This way, logic for all fields generated by django rest framework stays the same and you have anohter read only field. Nice and clean.
You will have to override .to_representation for your ModelSerializer class.
Docs
Source code
You want something just like this (modified original source) -
from rest_framework import serializers
from rest_framework.relations import PKOnlyObject
class MyModelSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
"""Object instance -> Dict of primitive datatypes."""
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
# We skip `to_representation` for `None` values so that fields do
# not have to explicitly deal with that case.
#
# For related fields with `use_pk_only_optimization` we need to
# resolve the pk value.
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
if check_for_none is None:
value = None
else:
value = field.to_representation(attribute)
ret[field.field_name] = {
'value': value,
# You can find more field attributes here
# https://github.com/encode/django-rest-framework/blob/master/rest_framework/fields.py#L324
'verbose_name': field.label,
'read_only': field.read_only,
'write_only': field.write_only,
'help_text': field.help_text,
}
return ret
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)