Ruddra.com

Django Serialize ForeignKey, ManyToMany, Instance, Property Method

Django Serialize ForeignKey, ManyToMany, Instance, Property Method
Photo by Daniel Dara on Unsplash

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

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

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, the 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

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(
        User, on_delete=models.CASCADE
    )
    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 FK 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'], indent=4
)
In [3]:print(serialized_data)
Out [4]:
[
    {
        "model": "experiment.blog",
        "pk": 1,
        "fields": {
            "author": "User",
            "tags": [
                "Tag 1",
                "Tag 2"
            ]
        }
    }
]

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 of property or instance method

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 ‘ForeignKey’(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 for specific fields in ForeignKey

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 of ‘XML’ serializer

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 ForeignKey fields

Show ForeignKey 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:

from rest_framework import serializers


class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = '__all__'

class BlogSerializer(serializers.ModelSerializer):
    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(serializers.ModelSerializer):
    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!!

Last updated: Apr 05, 2024


← Previous
Deploy Django App in Sub Directory Using OpenShift

Deploy your Django application in sub directory ie at path '/your-path' using OpenShift

Next →
Use VS Code Inside Docker Container for Development

VS Code is the most popular IDE at the moment. You can use it for developing applications in almost …

Share Your Thoughts
M↓ Markdown