django-rest-framework + django-polymorphic ModelSerialization

Question:

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.

Answers:

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.

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)
Answered By: darkless

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,
        }
Answered By: Syas