Hacks by Ruddra

Django Serialize ForeignKeyField, ManyToManyField, Instance and Property Method

Django’s serialization framework provides a mechanism for “translating” Django models into other formats. Usually they are in json, yaml, XML, GeoJSON etc text based formats.

Here, we are going to supercharge these serializers to do more things, even try to render properties and instance methods.

This article will be useful to you if you are:

  1. Interested to output some data in serialized formats(given above) and don’t want to integrate any third party libraries for that.
  2. Want to Develop an API without any help of third party libraries.
  3. Using Django serializer and need to output extra values from property/instance methods.
  4. Not really a big fan of third party libraries.

This article is divided into three parts, first one is how to use serializers to show ManyToMany and ForignKey Fields. In second part, we are going to show how to override the serializers to display property and instance methods. In third section, there are some small examples using DRF(a third party library), if you are in real hurry.

Proper Way: The Natural Implementation

For example purpose, let’s create three models:

from django.db import models
from django.contrib.auth import get_user_model

class User(models.Model):
    name = models.CharField(max_length=255)

class Tag(models.Model):
    name = models.CharField(max_length=255)

class Blog(models.Model):
    author = models.ForeignKey(
        get_user_model()
    )
    tags = models.ManyToManyField(Tag)
    text = models.TextField()

    @property
    def a_property_method(self):
        return "I am a Property Method"

    def an_instance_method(self):
        return "I am a Instance Method"

To use django’s implementation of rendering ForeignKey(we will address it as FK) and ManyToMany(we will address it as M2M) fields, we need to add natural_key method inside models:

class User(models.Model):
    name = models.CharField(max_length=255)

    def natural_key(self):
        return (self.name)

class Tag(models.Model):
    name = models.CharField(max_length=255)

    def natural_key(self):
        return (self.name)

Then, if we try to render ForignKey and M2M field like this:

In [1]:from django.core import serializers
In [2]:serialized_data = serializers.serialize('json', Blog.objects.all(),
    use_natural_foreign_keys=True, fields=['author', 'tags']
)
In [3]:print(serialized_data)
Out [4]: '[{"model": "Blog", "pk": 1, "fields": {"tags": ["tag1", "tag2"], "author": ["user1"]}}, {"model": "Blog", "pk": 2, "fields": {"tags": ["tag3", "tag4"], "author": ["user2"]}}]'

FYI, django serializer by default will not render @property or instance methods.

Our Way: Overriding Serializer Classes

This is our way by overriding the serializer class.

Render Property or Instance Method(For JSON, Python, GeoJSON, YAML serializer)

To render @property or instance methods, we have to override the Serializer classes. We are going to override the def end_object(self, obj): method of any serializer.

from django.core.serializers.json import Serializer
from django.db.models import Manager
# FYI: It can be any of the following as well:
# from django.core.serializers.pyyaml import Serializer
# from django.core.serializers.python import Serializer
# from django.contrib.gis.serializers.geojson import Serializer

class CustomSerializer(Serializer):

    def end_object(self, obj):
        for field in self.selected_fields:
            if field == 'pk':
                continue
            elif field in self._current.keys():
                continue
            else:
                try:
                    self._current[field] = getattr(obj, field)()  # for model methods
                    continue
                except TypeError:
                    pass
                try:
                    self._current[field] = getattr(obj, field)  # for property methods
                    continue
                except AttributeError:
                    pass
        super(CustomSerializer, self).end_object(obj)

Usage:

serializers = CustomSerializer()
queryset = Blog.objects.all()
data = serializers.serialize(queryset, fields=('a_property_method', 'an_instance_method'))

Only Render Specific Fields Using __field_name in FK(For JSON, Python, GeoJSON, YAML serializer)

In this approach, we are going to take pass name of the fields inside FK which we want to display in our response as forignkey__field_name. Passing these parameters going to look like this: fields=['fk__name1', 'fk__name2'] etc. So, let’s override the Serializer

from django.contrib.gis.serializers.geojson import Serializer
from django.db.models import Manager
# FYI: It can be any of the following as well:
# from django.core.serializers.pyyaml import Serializer
# from django.core.serializers.python import Serializer
# from django.core.serializers.json import Serializer

JSON_ALLOWED_OBJECTS = (dict,list,tuple,str,int,bool)


class CustomSerializer(Serializer):

    def end_object(self, obj):
        for field in self.selected_fields:
            if field == 'pk':
                continue
            elif field in self._current.keys():
                continue
            else:
                try:
                    if '__' in field:
                        fields = field.split('__')
                        value = obj
                        for f in fields:
                            value = getattr(value, f)
                        if value != obj and isinstance(value, JSON_ALLOWED_OBJECTS) or value == None:
                            self._current[field] = value

                except AttributeError:
                    pass
        super(CustomSerializer, self).end_object(obj)

Usage

Usage will look like this:

serializers = CustomSerializer()
queryset = Blog.objects.all()
data = serializers.serialize(queryset, fields=('author__name', 'text'))

Output

[
  {
    "author__name": "user1",
    "text": "Some Text"
  },
  {
    "author__name": "user2",
    "text": "Some Text"
  }
]

Show Property and Instance Method(For XML Serializer)

Same as JSON, Python, GeoJSON, YAML serializer, here we will override the end_object method. But in a slightly different way:

from django.core.serializers.xml_serializer import Serializer

class CustomSerializer(Serializer):
    def _init_options(self):
        self.non_model_fields = self.options.get('non_model_fields', [])

    def start_serialization(self):
        self._init_options()
        super(CustomSerializer, self).start_serialization()

    def end_object(self, obj):
        for field in self.non_model_fields:
            value = None
            try:
                value = getattr(obj, field)()  # object method
                continue
            except TypeError:
               pass

            try:
               value = getattr(obj, field)  # property method
               continue
            except AttributeError:
               pass

            self.indent(2)
            self.xml.startElement('field', {
                'name': field,
                'type': value.__class__.__name__,
            })
            if value:
                self.xml.characters(str(value))
            else:
                self.xml.addQuickElement("None")

            self.xml.endElement("field")
        return super(CustomSerializer, self).end_object(obj)

Usage

serializers = CustomSerializer()
queryset = Blog.objects.all()
data = serializers.serialize(queryset, non_model_fields=('a_property_method', 'an_instance_method'))

Third Party: Quicker Way

We will be using Django Rest Framework and their serializer in this section.

Show ForignKey Fields

Show ForignKey values using depth.

class BlogSerializer(models.Model):
    class Meta:
        model = Blog
        fields = '__all__'
        depth = 1

Show M2M Fields

We can define a serializer for Nested class to be shown in M2M. Like this:

class TagSerializer(models.Model):
    class Meta:
        model = Tag
        fields = '__all__'

class BlogSerializer(models.Model):
    tags = TagSerializer(many=True)
    class Meta:
        model = Blog
        fields = '__all__'
        depth = 1

Show Property and Instance Methods

We can do that by specifying an extra field:

class BlogSerializer(models.Model):
    a_property_method = serializers.CharField(source='a_property_method', read_only=True)
    an_instance_method = serializers.CharField(source='an_instance_method', read_only=True)
    class Meta:
        model = Blog
        fields = '__all__'

Thats all for today, thank you for reading. You can checkout my answers on this topic on StackOverflow:

  1. https://stackoverflow.com/a/56557206/2696165
  2. https://stackoverflow.com/a/53352643/2696165

If you have any question, please ask in the comments section below, Cheers!!