Blog | CoWhite Softwarehttp://django.cowhite.com/blog/2017-07-05T21:04:09+00:00BlogDynamic fields in Django Rest Framwork serializers2017-07-05T21:04:09+00:00ravi/blog/author/ravi/http://django.cowhite.com/blog/dynamic-fields-in-django-rest-framwork-serializers/<p>Often we need to have custom fields that must be calculated dynamically. In Entity-Relationship model notation the fields are called
<strong><em>Derived Attributes</em></strong>. Such fields are derived from other fields of the same entity (model) and they might change over time (age, for example). We can create dynamic fields in django in more than one way.</p>
<p><strong> 1. Dynamic field as a model property</strong></p>
<p>Let us create a model <code>Author</code> as follows.</p>
<div class="codehilite"><pre><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">django.utils</span> <span class="kn">import</span> <span class="n">timezone</span>
<span class="k">class</span> <span class="nc">Author</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">40</span><span class="p">)</span>
<span class="n">dob</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateField</span><span class="p">(</span><span class="n">verbose_name</span><span class="o">=</span><span class="s">'Date of birth'</span><span class="p">)</span>
</pre></div>
<p>We need a field that gives us the age. An obvious choice for this is to create a method that returns the age calculated based on the <code>dob</code> field value. But Python has the <a href="https://docs.python.org/2/library/functions.html#property">property decorator</a> that allows us to access a class methods as if it is a class attribute. It also allows us to write setter and getter for the attribute, but our case is simple and we can use it to create a read-only property that can act as a field.</p>
<p>Add the following method along with the decorator.</p>
<div class="codehilite"><pre>@property
def age(self):
return timezone.now().year - self.dob.year
</pre></div>
<p>Finally the model looks like this.</p>
<div class="codehilite"><pre><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">django.utils</span> <span class="kn">import</span> <span class="n">timezone</span>
<span class="k">class</span> <span class="nc">Author</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">40</span><span class="p">)</span>
<span class="n">dob</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateField</span><span class="p">(</span><span class="n">verbose_name</span><span class="o">=</span><span class="s">'Date of birth'</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">age</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="o">.</span><span class="n">year</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">dob</span><span class="o">.</span><span class="n">year</span>
</pre></div>
<p>This property (method) will be treated as if it is a field of a model by Django and Django Rest Framework serializer. Create the following serializer for the model.</p>
<div class="codehilite"><pre><span class="kn">from</span> <span class="nn">rest_framework</span> <span class="kn">import</span> <span class="n">serializers</span>
<span class="kn">from</span> <span class="nn">.models</span> <span class="kn">import</span> <span class="n">Post</span><span class="p">,</span> <span class="n">Author</span>
<span class="k">class</span> <span class="nc">AuthorSerializer</span><span class="p">(</span><span class="n">serializers</span><span class="o">.</span><span class="n">ModelSerializer</span><span class="p">):</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Author</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">(</span><span class="s">'id'</span><span class="p">,</span> <span class="s">'name'</span><span class="p">,</span> <span class="s">'dob'</span><span class="p">,</span> <span class="s">'age'</span><span class="p">)</span>
</pre></div>
<p>Note that <code>age</code> field has to be explicitly mentioned. If <code>fields</code> is set to <code>'__all__'</code> string, the serializer does not include the <code>age</code> field. </p>
<p><img alt="Author serializer" src="http://django.cowhite.com/static/media/uploads/Serailizers%20and%20custom%20fields/screenshot_from_2017-07-05_18-53-37-min.png" /></p>
<p><strong>2. Using <code>SerializerMethodField</code></strong></p>
<p>Django Rest Framework gives us the <code>SerializerMethodField</code> field whose value is dynamically evaluated by the serializer itself and not the model. Using this method has an advantage because the value can be generated in the context of the current session.</p>
<p>Let us revert back to the original <code>Author</code> model that does not include the <code>age</code> property.</p>
<div class="codehilite"><pre><span class="k">class</span> <span class="n">Author</span>(<span class="n">models</span>.<span class="n">Model</span>):
<span class="nb">name</span> = <span class="n">models</span>.<span class="n">CharField</span>(<span class="n">max_length</span>=<span class="mi">40</span>)
<span class="n">dob</span> = <span class="n">models</span>.<span class="n">DateField</span>(<span class="n">verbose_name</span>=<span class="s">'Date of birth'</span>)
</pre></div>
<p>We use the serializer to calculate the age and pass it as a serializer field.</p>
<div class="codehilite"><pre><span class="k">class</span> <span class="n">AuthorSerializer</span>(<span class="n">serializers</span>.<span class="n">ModelSerializer</span>):
<span class="n">age</span> = <span class="n">serializers</span>.<span class="n">SerializerMethodField</span>()
<span class="k">class</span> <span class="n">Meta:</span>
<span class="n">model</span> = <span class="n">Author</span>
<span class="n">fields</span> = <span class="s">'__all__'</span>
<span class="n">def</span> <span class="n">get_age</span>(<span class="k">self</span>, <span class="n">instance</span>):
<span class="k">return</span> <span class="n">datetime</span>.<span class="n">datetime</span>.<span class="n">now</span>().<span class="n">year</span> - <span class="n">instance</span>.<span class="n">dob</span>.<span class="n">year</span>
</pre></div>
<p>Any <code>SerializerMethodField</code> will look for a <code>get_<field_name></code> method and use it as source. This is equivalent to our previous procedure.</p>
<p><img alt="Author serializer" src="http://django.cowhite.com/static/media/uploads/Serailizers%20and%20custom%20fields/screenshot_from_2017-07-05_18-53-37-min.png" /></p>
<p>We can also use the current session in the serializer. Let us change the method to show the age only to admins. Show 'hidden' for other users. Change the <code>get_age</code> method as to the following.</p>
<div class="codehilite"><pre>def calculate_age(self, instance):
request = self.context.get('request')
user = request.user
if user.is_authenticated() and user.is_admin:
return datetime.datetime.now().year - instance.dob.year
return 'Hidden'
</pre></div>
<p>and the age field to <code>age = serializers.SerializerMethodField(method_name='calculate_age')</code>. The full serializer looks like this.</p>
<div class="codehilite"><pre><span class="k">class</span> <span class="n">AuthorSerializer</span>(<span class="n">serializers</span>.<span class="n">ModelSerializer</span>):
<span class="n">age</span> = <span class="n">serializers</span>.<span class="n">SerializerMethodField</span>(<span class="n">method_name</span>=<span class="s">'calculate_age'</span>)
<span class="k">class</span> <span class="n">Meta:</span>
<span class="n">model</span> = <span class="n">Author</span>
<span class="n">fields</span> = (<span class="s">'id'</span>, <span class="s">'name'</span>, <span class="s">'dob'</span>, <span class="s">'age'</span>)
<span class="n">def</span> <span class="n">calculate_age</span>(<span class="k">self</span>, <span class="n">instance</span>):
<span class="n">request</span> = <span class="k">self</span>.<span class="nb">context</span>.<span class="n">get</span>(<span class="s">'request'</span>)
<span class="n">user</span> = <span class="n">request</span>.<span class="n">user</span>
<span class="k">if</span> <span class="n">user</span>.<span class="n">is_authenticated</span>() <span class="o">and</span> <span class="n">user</span>.<span class="n">is_staff:</span>
<span class="k">return</span> <span class="n">datetime</span>.<span class="n">datetime</span>.<span class="n">now</span>().<span class="n">year</span> - <span class="n">instance</span>.<span class="n">dob</span>.<span class="n">year</span>
<span class="k">return</span> <span class="s">'Hidden'</span>
</pre></div>
<p>The result for any user (including AnonymousUser) is as follows.</p>
<p><img alt="Serializer for AnonymousUser" src="http://django.cowhite.com/static/media/uploads/Serailizers%20and%20custom%20fields/screenshot_from_2017-07-05_19-06-53-min_(1).png" /></p>
<p>The result for admin looks like this.</p>
<p><img alt="Serializer for admin" src="http://django.cowhite.com/static/media/uploads/Serailizers%20and%20custom%20fields/screenshot_from_2017-07-05_18-53-37-min.png" /></p>
<p>Surely there are more ways to generate dynamic fields, using <a href="https://docs.djangoproject.com/en/dev/topics/db/aggregation/#generating-aggregates-for-each-item-in-a-queryset">annotations</a> for example. These are the simplest ways to achieve this at the application level. Hope this helps.</p>