django-rest-framework, multitable model inheritance, ModelSerializers and nested serializers
Asked Answered
M

5

25

I can't find this info in the docs or on the interwebs.
latest django-rest-framework, django 1.6.5

How does one create a ModelSerializer that can handle a nested serializers where the nested model is implemented using multitable inheritance?

e.g.

######## MODELS
class OtherModel(models.Model):
    stuff = models.CharField(max_length=255)

class MyBaseModel(models.Model):
    whaddup = models.CharField(max_length=255)
    other_model = models.ForeignKey(OtherModel)

class ModelA(MyBaseModel):
    attr_a = models.CharField(max_length=255)

class ModelB(MyBaseModel):
    attr_b = models.CharField(max_length=255)


####### SERIALIZERS
class MyBaseModelSerializer(serializers.ModelSerializer):
    class Meta:
        model=MyBaseModel

class OtherModelSerializer(serializer.ModelSerializer):
    mybasemodel_set = MyBaseModelSerializer(many=True)

    class Meta:
        model = OtherModel

This obviously doesn't work but illustrates what i'm trying to do here.
In OtherModelSerializer, I'd like mybasemodel_set to serialize specific represenntations of either ModelA or ModelB depending on what we have.

If it matters, I'm also using django.model_utils and inheritencemanager so i can retrieve a queryset where each instance is already an instance of appropriate subclass.

Thanks

Mroz answered 4/6, 2014 at 22:22 Comment(0)
A
24

I've solved this issue a slightly different way.

Using:

  • DRF 3.5.x
  • django-model-utils 2.5.x

My models.py look like this:

class Person(models.Model):
    first_name = models.CharField(max_length=40, blank=False, null=False)
    middle_name = models.CharField(max_length=80, blank=True, null=True)
    last_name = models.CharField(max_length=80, blank=False, null=False)
    family = models.ForeignKey(Family, blank=True, null=True)


class Clergy(Person):
    category = models.IntegerField(choices=CATEGORY, blank=True, null=True)
    external = models.NullBooleanField(default=False, null=True)
    clergy_status = models.ForeignKey(ClergyStatus, related_name="%(class)s_status", blank=True, null=True)


class Religious(Person):
    religious_order = models.ForeignKey(ReligiousOrder, blank=True, null=True)
    major_superior = models.ForeignKey(Person, blank=True, null=True, related_name="%(class)s_superior")


class ReligiousOrder(models.Model):
    name = models.CharField(max_length=255, blank=False, null=False)
    initials = models.CharField(max_length=20, blank=False, null=False)


class ClergyStatus(models.Model):
    display_name = models.CharField(max_length=255, blank=True, null=True)
    description = models.CharField(max_length=255, blank=True, null=True)

Basically - The base model is the "Person" model - and a person can either be Clergy, Religious, or neither and simply be a "Person". While the models that inherit Person have special relationships as well.

In my views.py I utilize a mixin to "inject" the subclasses into the queryset like so:

class PersonSubClassFieldsMixin(object):

    def get_queryset(self):
        return Person.objects.select_subclasses()

class RetrievePersonAPIView(PersonSubClassFieldsMixin, generics.RetrieveDestroyAPIView):
    serializer_class = PersonListSerializer
    ...

And then real "unDRY" part comes in serializers.py where I declare the "base" PersonListSerializer, but override the to_representation method to return special serailzers based on the instance type like so:

class PersonListSerializer(serializers.ModelSerializer):

    def to_representation(self, instance):
        if isinstance(instance, Clergy):
            return ClergySerializer(instance=instance).data
        elif isinstance(instance, Religious):
            return ReligiousSerializer(instance=instance).data
        else:
            return LaySerializer(instance=instance).data

    class Meta:
        model = Person
        fields = '__all__'


class ReligiousSerializer(serializers.ModelSerializer):
    class Meta:
        model = Religious
        fields = '__all__'
        depth = 2


class LaySerializer(serializers.ModelSerializer):
    class Meta:
        model = Person
        fields = '__all__'


class ClergySerializer(serializers.ModelSerializer):
    class Meta:
        model = Clergy
        fields = '__all__'
        depth = 2

The "switch" happens in the to_representation method of the main serializer (PersonListSerializer). It looks at the instance type, and then "injects" the needed serializer. Since Clergy, Religious are all inherited from Person getting back a Person that is also a Clergy member, returns all the Person fields and all the Clergy fields. Same goes for Religious. And if the Person is neither Clergy or Religious - the base model fields are only returned.

Not sure if this is the proper approach - but it seems very flexible, and fits my usecase. Note that I save/update/create Person thru different views/serializers - so I don't have to worry about that with this type of setup.

Analyzer answered 16/2, 2017 at 18:58 Comment(3)
where do you use django-model-utils 2.5.?Uncompromising
In the django-model-utils documentation, we can see that Person.objects.select_subclasses() is added by the InheritanceManager that Jeff may have forget to use in his example. So, in the Person class, you may find a line containing objects = InheritanceManager() I suppose.Hooks
Ideally you'd also need to deal with to_internal_value()Infect
M
8

I was able to do this by creating a custom relatedfield

class MyBaseModelField(serializers.RelatedField):
    def to_native(self, value):
        if isinstance(value, ModelA):
            a_s = ModelASerializer(instance=value)
            return a_s.data
        if isinstance(value, ModelB):
            b_s = ModelBSerializer(instance=value)
            return b_s.data

        raise NotImplementedError


class OtherModelSerializer(serializer.ModelSerializer):
    mybasemodel_set = MyBaseModelField(many=True)

    class Meta:
        model = OtherModel
        fields = # make sure we manually include the reverse relation (mybasemodel_set, )

I do have concerns that instanting a Serializer for each object is the reverse relation queryset is expensive so I'm wondering if there is a better way to do this.

Another approach i tried was dynamically changing the model field on MyBaseModelSerializer inside of __init__ but I ran into the issue described here:
django rest framework nested modelserializer

Mroz answered 5/6, 2014 at 18:23 Comment(3)
django-rest-framework.org/topics/3.0-announcement/…Standstill
did you find a better solution yet?Queen
Updated link of broken link in comment #1: DRF3.0 - changes to the custom field APIBelen
I
3

Using Django 3.1, I found that it is possible to override get_serializer instead of get_serializer_class, in which case you can access the instance as well as self.action and more.

By default get_serializer will call get_serializer_class, but this behavior can be adjusted to your needs.

This is cleaner and easier than the solutions proposed above, so I'm adding it to the thread.

Example:

class MySubclassViewSet(viewsets.ModelViewSet):
    # add your normal fields and methods ...

    def get_serializer(self, *args, **kwargs):
        if self.action in ('list', 'destroy'):
            return MyListSerializer(args[0], **kwargs)
        if self.action in ('retrieve', ):
            instance = args[0]
            if instance.name.contains("really?"):  # or check if instance of a certain Model...
                return MyReallyCoolSerializer(instance)
            else return MyNotCoolSerializer(instance)
        # ... 
        return MyListSerializer(*args, **kwargs)  # default
Infect answered 30/9, 2020 at 15:15 Comment(0)
E
1

I'm attempting to use a solution that involves different serializer subclasses for the different model subclasses:

class MyBaseModelSerializer(serializers.ModelSerializer):

    @staticmethod
    def _get_alt_class(cls, args, kwargs):
        if (cls != MyBaseModel):
            # we're instantiating a subclass already, use that class
            return cls

        # < logic to choose an alternative class to use >
        # in my case, I'm inspecting kwargs["data"] to make a decision
        # alt_cls = SomeSubClass

        return alt_cls

    def __new__(cls, *args, **kwargs):
        alt_cls = MyBaseModel.get_alt_class(cls, args, kwargs)
        return super(MyBaseModel, alt_cls).__new__(alt_cls, *args, **kwargs)

    class Meta:
        model=MyBaseModel

class ModelASerializer(MyBaseModelSerializer):
    class Meta:
        model=ModelA

class ModelBSerializer(MyBaseModelSerializer):
    class Meta:
        model=ModelB

That is, when you try and instantiate an object of type MyBaseModelSerializer, you actually end up with an object of one of the subclasses, which serialize (and crucially for me, deserialize) correctly.

I've just started using this, so it's possible that there are problems I've not run into yet.

Excursion answered 27/4, 2016 at 10:29 Comment(1)
why making get_alt_class a staticmethod to which you end up passing cls ? Any reason ?Clubhaul
T
0

I found this post via Google trying to figure out how to handle multiple table inheritance without having to check the model instance type. I implemented my own solution.

I created a class factory and a mixin to generate the serializers for the child classes with the help of InheritanceManger from django-model-utils.

models.py

from django.db import models
from model_utils import InheritanceManager


class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

    # Use the InheritanceManager for select_subclasses()
    objects = InheritanceManager()  

class Restaurant(Place):
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

serializers.py

from rest_framework import serializers

from .models import Location

def modelserializer_factory(model, class_name='ModelFactorySerializer',
                            meta_cls=None, **kwargs):
    """Generate a ModelSerializer based on Model"""
  
    if meta_cls is None:
        # Create a Meta class with the model passed
        meta_cls = type('Meta', (object,), dict(model=model))
    elif not hasattr(meta_cls, 'model'):
        # If a meta_cls is provided but did not include a model,
        # set it to the model passed into this function
        meta_cls.model = model

    # Create the ModelSerializer class with the Meta subclass
    # we created above; also pass in any additional keyword
    # arguments via kwargs
    ModelFactorySerializer = type(class_name, (serializers.ModelSerializer,),
                                  dict(Meta=meta_cls, **kwargs))
    ModelFactorySerializer.__class__.__name__ = class_name
    return ModelFactorySerializer


class InheritedModelSerializerMixin:
    def to_representation(self, instance):
        # Get the model of the instance
        model = instance._meta.model
        
        # Override the model with the inherited model
        self.Meta.model = model
           
        # Create the serializer via the modelserializer_factory
        # This will use the name of the class this is mixed with.
       
        serializer = modelserializer_factory(model, self.__class__.__name__,
                                             meta_cls=self.Meta)
        # Instantiate the Serializer class with the instance
        # and return the data
        return serializer(instance=instance).data


# Mix in the InheritedModelSerializerMixin
class LocationSerializer(InheritedModelSerializerMixin, serializers.ModelSerializer):
    class Meta:
        model = Location   # 'model' is optional since it will use
                           # the instance's model

        exclude = ('serves_pizza',)  # everything else works as well
        depth = 2                    # including depth

views.py

from .models import Location
from .serializers import LocationSerializer


# Any view should work.
# This is an example using viewsets.ReadOnlyModelViewSet
# Everything else works as usual. You will need to chain
# ".select_subclasses()" to the queryset to select the
# child classes.

class LocationViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Location.objects.all().select_subclasses() 
    serializer_class = LocationSerializer
Topdress answered 31/7, 2022 at 5:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.