21

Description of the problem

Taxonomy is the science of defining and naming groups of biological organisms on the basis of shared characteristics. Organisms are grouped together into taxa (singular: taxon) and these groups are given a taxonomic rank. The principal ranks in modern use are domain, kingdom, phylum, class, order, family, genus and species. More information on Taxonomy and Taxonomic ranks in Wikipedia.

Following the example for the red fox in the article Taxonomic rank in Wikipedia I need to create a JSON output like this:

{
    "species": "vulpes",
    "genus": "Vulpes",
    "family": "Canidae",
    "order": "Carnivora",
    "class": "Mammalia",
    "phylum": "Chordata",
    "kingdom": "Animalia",
    "domain": "Eukarya"
}

Since Django REST Framework creates the keys based on the field names, the problem arises with the taxonomic rank class (bold in the example) as it is a reserved word in Python and can't be used as a variable name.

What I have tried

A model class created in Django would look like this:

class Species(models.Model):
    species = models.CharField()
    genus = models.CharField()
    family = models.CharField()
    # class = models.CharField() - class is reserved word in Python
    # class_ = models.CharField() - Django doesn't allow field names
    # ending with underscore. That wouldn't be either a satisfying solution.
    # further fields

Question

Is there any possible way to solve this problem and generate the desired output? If not, what is the best practice to work around this problem?

cezar
  • 11,616
  • 6
  • 48
  • 84
  • 1
    See [this question](https://stackoverflow.com/questions/39324691/rename-response-fields-django-rest-framework-serializer) for how to rename the field in django-rest-framework. You could use something like `species_class` or `klass` for your Django model field. – Alasdair Dec 04 '17 at 10:21
  • @Alasdair Thank you very much, pal! With the help of the serializer method `to_representation` I have found a solution. The model field I call now `class_name`, which doesn't bother, as the client takes care only for the output. Would you add a couple of lines as answer, so I can upvote/accept it. – cezar Dec 04 '17 at 10:53
  • 2
    Glad that helped. It's probably better if you add your own answer, because you can show what you actually did to get it working. – Alasdair Dec 04 '17 at 11:01

4 Answers4

18

You can rename field in the overloaded version of get_fields() method

class MySerializer(serializers.Serializer):
    class_ = serializers.ReadOnlyField()

    def get_fields(self):
        result = super().get_fields()
        # Rename `class_` to `class`
        class_ = result.pop('class_')
        result['class'] = class_
        return result
dtatarkin
  • 1,201
  • 8
  • 6
14

You can set the property of a class via strings as such:

class SpeciesSerializer(serializers.Serializer):
    species = serializers.CharField()
    genus = serializers.CharField()
    family = serializers.CharField()
    vars()['class'] = serializers.CharField()
  • 1
    The simplest and the easiest method ever which is most suitable to the Python philosophy. Thank you for the tricks with the `vars()` function (or equivalent `locals()`). – mymedia May 24 '22 at 15:33
  • This is such a beautiful and clean solution! It's crazy it took 4 years for this to be posted but I'm all here for it! – Akaisteph7 Aug 14 '23 at 20:32
  • Note that there are concerns about modifying the results of `vars()` and `locals()` ([source](https://docs.python.org/3/library/functions.html#locals)) but this seems like a valid and safe modification to me. As long as your migration file gets correctly generated then you should be good to go. – Akaisteph7 Aug 14 '23 at 20:54
6

You can do it like below

class SpeciesSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Species
        fields = (
            'url', 'id', 'canonical_name', 'slug',  'species', 'genus',
            'subfamily', 'family', 'order','class', 'phylum',
            'ncbi_id', 'ncbi_taxonomy',
        )
        read_only_fields = ('slug',)
        extra_kwargs = {
            'url': {'lookup_field': 'slug'}
        }

SpeciesSerializer._declared_fields["class"] = serializers.CharField(source="class_name")

As explained in below answer

https://stackoverflow.com/a/47717441/2830850

Tarun Lalwani
  • 142,312
  • 9
  • 204
  • 265
5

Other software developers in the field of Bioinformatics might be interested in a solution of this problem, so I post here my approach as suggested by Alasdair.

The goal is to create a model for a living species, for the sake of simplicity let's say an animal, and create an endpoint with Django REST Framework representing the correct taxonomic ranks.

models.py

from django.db import models

class Animal(models.Model):
    canonical_name = models.CharField(max_length=100, unique=True)
    species = models.CharField(max_length=60, unique=True)
    genus = models.CharField(max_length=30)
    family = models.CharField(max_length=30)
    order = models.CharField(max_length=30)
    # we can't use class as field name
    class_name = models.CharField('Class', db_column='class', max_length=30)
    phylum = models.CharField(max_length=30)
    # we don't need to define kingdom and domain
    # it's clear that it is an animal and eukaryote

    def __str__(self):
        return '{} ({})'.format(self.canonical_name, self.species)

serializers.py

from collections import OrderedDict

from rest_framework import serializers

from .models import Species

class SpeciesSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Animal
        fields = ('url', 'id', 'canonical_name', 'species', 'genus',
            'subfamily', 'family', 'order', 'class_name', 'phylum')

    def to_representation(self, obj):
        # call the parent method and get an OrderedDict
        data = super(SpeciesSerializer, self).to_representation(obj)
        # generate a list of the keys and replace the key 'class_name'
        keys = list(data.keys())
        keys.insert(keys.index('class_name'), 'class')
        keys.remove('class_name')
        # remove 'class_name' and assign its value to a new key 'class'
        class_name = data.pop('class_name')
        data.update({'class': class_name})
        # create new OrderedDict with the order given by the keys
        response = OrderedDict((k, data[k]) for k in keys)
        return response

The method to_representation helps us to manipulate the output. I have put some extra work here to get the taxonomic ranks in the desired order.

Thus for the red fox the output looks like this:

Red fox (Vulpes vulpes)

{
    "url": "http://localhost:8000/animal/1",
    "id": 1,
    "canonical_name": "Red fox",
    "species": "Vulpes vulpes",
    "genus": "Vulpes",
    "family": "Canidae",
    "order": "Carnivora",
    "class": "Mammalia",
    "phylum": "Chordata"
}

It is a simplified example and in reality you'd have many more fields or possibly a model for every taxonomic rank, but somewhere you might come across the conflict between the reserved word class and the taxonomic rank class.
I hope this can help other people too.

cezar
  • 11,616
  • 6
  • 48
  • 84