Create a generic serializer with a dynamic model in Meta

Question:

when i create a Serializer in django-rest0-framework, based on a ModelSerializer, i will have to pass the model in the Meta class:

class ClientSerializer(ModelSerializer):
    class Meta:
        model = Client

I want to create a general serializer which, based on the URL, includes the model dynamically.

My setup thusfar includes the urls.py and the viewset:

urls.py:

 url(r'^api/v1/general/(?P<model>w+)', kernel_api_views.GeneralViewSet.as_view({'get':'list'}))

and views.py:

class GeneralViewSet(viewsets.ModelViewSet):

     def get_queryset(self):
            # Dynamically get the model class from myapp.models
            queryset = getattr(myapp.models, model).objects.all()
            return queryset

     def get_serializer_class(self):
         return getattr(myapp.serializers, self.kwargs['model']+'Serializer')

Which in case of: http://127.0.0.1:8000/api/v1/general/Client gets Client.objects.all() as queryset and the ClientSerializer class as serializer

Question: How can i make it so that i can call ‘GeneralSerializer’ and dynamically assign the model in it?

Asked By: Robin van Leeuwen

||

Answers:

Create general serializer without model in Meta:

class GeneralModelSerializer(serializers.ModelSerializer):
    ...

Add model to serializer in “:

class GenericViewSet(viewsets.ModelViewSet):

    def get_serializer_class(self):
        serializer_class = GeneralModelSerializer
        serializer_class.Meta.model = YourModel
        return serializer_class
Answered By: zymud

You can do that by following:

serializers.py

class GeneralSerializer(serializers.ModelSerializer):

    class Meta:
        model = None

views.py

class GeneralViewSet(viewsets.ModelViewSet):

     def get_queryset(self):
         model = self.kwargs.get('model')
         return model.objects.all()           

     def get_serializer_class(self):
         GeneralSerializer.Meta.model = self.kwargs.get('model')
         return GeneralSerializer  

In serializers.py, we define a GeneralSerializer having model in Meta as None. We’ll override the model value at the time of calling get_serializer_class().

Then in our views.py file, we define a GeneralViewSet with get_queryset() and get_serializer_class() overridden.

In get_queryset(), we obtain the value of the model from kwargs and return that queryset.

In get_serializer_class(), we set the value of model for GeneralSerializer to the value obtained from kwargs and then return the GeneralSerializer.

Answered By: Rahul Gupta

So far I know you cannot create a generic serializer if you use model serializer, but you can get the same solution using a base class and deriving all your models from that base class. Implement a method to return the serializer and then use that method to generate a dynamic serializer. I am using this technique for the last 2 years and working pretty fine for me –

class BaseModel(models.Model):
    class Meta:
         abstract = True # define abstract so that it does not cause any problem with model hierarchy in database

    @classmethod
    def get_serializer(cls):
         class BaseSerializer(serializers.ModelSerializer):
               class Meta:
                    model = cls # this is the main trick here, this is how I tell the serializer about the model class

         return BaseSerializer #return the class object so we can use this serializer

Now derive your models from it –

class Derived1(BaseModel):
    pass

class Derived2(BaseModel):
    pass

if you want to override the serializer then just do it in the one that you need. for example –

class DerivedOverride(BaseModel):
    @classmethod
    def get_serializer(cls):
         super_serializer = BaseModel.get_serializer() # this important to not to break the serializing hierarchy
         class BaseSerializer(super_serializer):
               class Meta:
                    model = cls # this is the main trick here, this is how I tell the serializer about the model class

         return BaseSerializer

Thats it, now each class has its own dynamic serializer but we just defined it in one place.

Now use the serializer in view set –

class Derive1ViewSet(ModelViewSet):
    serializer_class = Derived1.get_serializer()

class Derive2ViewSet(ModelViewSet):
    serializer_class = Derived2.get_serializer()

and go on from there.

Answered By: brainless coder

To build on Rahul’s answer, this is what worked for me:

urls.py

url(r'^api/(?P<app_label>w+)/(?P<model_name>w+)', GeneralViewSet.as_view({'get': 'list'}))

serializers.py

from rest_framework import serializers
class GeneralSerializer(serializers.ModelSerializer):

    class Meta:
        model = None

views.py

from django.apps import apps        
class GeneralViewSet(viewsets.ModelViewSet):

    @property
    def model(self):
        return apps.get_model(app_label=str(self.kwargs['app_label']), model_name=str(self.kwargs['model_name']))

    def get_queryset(self):
        model = self.model
        return model.objects.all()           

    def get_serializer_class(self):
        GeneralSerializer.Meta.model = self.model
        return GeneralSerializer
Answered By: btal

My solution:

Create general serializer with or without model, send model name in url

plugin: django-geojson==2.12.0
django: 2.0.6
python: 3.6.7

api.py

from djgeojson.serializers import Serializer as GeoJSONSerializer
from django.http import HttpResponse
from django.db import connection


def to_geojson(request):
        model = request.GET['model']
        print(model)
        lista = []
        with connection.cursor() as cursor:
            cursor.execute("SELECT * FROM %s" % (model))
            # to dict
            lista = dictfetchall(cursor)
            # Serialize
            geo_lista = GeoJSONSerializer().serialize(lista)
        return HttpResponse(geo_lista)


    def dictfetchall(cursor):
        "Return all rows from a cursor as a dict"
        columns = [col[0] for col in cursor.description]
        return [
            dict(zip(columns, row))
            for row in cursor.fetchall()
        ]

urls.py

url(r'^api/geo/', to_geojson, name='to_geojson')

my urls to call API:
with model in models.py

http://127.0.0.1:8000/data/api/geo/?model=data_pointcloud

without model in models.py

http://127.0.0.1:8000/data/api/geo/?model="schema".table_name
Answered By: Javi

In addition to @Rahul and @btal, we can use decorator pattern on ModelSerializer in case if we want to use APIView.

def getGenericSerializer(model_arg):
    class GenericSerializer(serializers.ModelSerializer):
        class Meta:
            model = model_arg
            fields = '__all__'

    return GenericSerializer

And use it like this in the APIView:

class MyView(APIView):
    def get(self, request, format=None):
        #...
        GenericSzl = getGenericSerializer(model)
        serializer = GenericSzl(objs, many=True)
        return Response(serializer.data)

Hope this helps to those people who don’t want to use ModelViewSet.

Answered By: Parth Joshi

For Django 3.0+

from yourapp import models

class GeneralViewSet(viewsets.ModelViewSet):

     def get_queryset(self):
         model = self.kwargs.get('model')
         return getattr(models, model).objects.all()         

     def get_serializer_class(self):
         model = self.kwargs.get('model')
         GeneralSerializer.Meta.model =  getattr(models, model)
         return GeneralSerializer  
Answered By: linncy