Dynamic Serializers in Django Rest Framework

9-17-2018

Three ways to make Serializers update on the fly

Making Serializers Dynamic With Django Rest Framework

Django Rest Framework is great. It provides all the batteries you need, and python lets you write powerful, clear, and easy-to-maintain code.

One challenge with building decoupled applications (generally, not just with DRF) is ensuring data is sent to the client with the correct permissions. Also, we may want to ensure that certain fields, that depend on expensive database queries or API calls, don't get loaded by default.

This is trivial to achieve at the object/class level (and particularly great to write with Dry-Rest-Permissions), but it's challenging to manage at the field level. Or how do you achieve allowing anonymous users to view only a subset of a given object's data?

Two standards in API design don't really help us: GraphQL and JSON-API's sparse fieldsets are all opt-in, making what I want to accomplish difficult.

Here are three approaches:

Per Field Permissions

The Django Rest Serializer Field Permissions Package allows the API author to apply specific authorization classes to a specific field.

 class PersonSerializer(FieldPermissionSerializerMixin, LookupModelSerializer):

      // Only allow authenticated users to retrieve family and given names
      family_names = serializers.CharField(permission_classes=(IsAuthenticated(), ))
      given_names = serializers.CharField(permission_classes=(IsAuthenticated(), ))

      // Allow all users to retrieve nick name
      nick_name = serializers.CharField(permission_classes=(AllowAll(), ))

Load Custom Serializer Class

If you want to provide a custom serializer — to reuse admin-specific fields — you can implement logic in the ViewSet's get_serializer_class method to swap out serializers based on the request object.

class GroupViewSet(OwnerBasedViewSet):
  def get_serializer_class(self):
    user = self.request.user
    return AdminSerializer if request.user.is_admin else NormalSerializer

This was is great for ensuring that external relationships and other sensitive fields that can be easy to forget are not exposed.

Include fields via query param

Following the convention of JSON:API, but not wanting to use the opt-in only approach of sparse-fieldsets, I wrote/copied a serializer mixin that parses the included query param for a list of keys, then grabs them from a DynamicIncluded class setup on the Serializer.

The mixin looks like:

class DynamicIncludedFieldsMixin(object):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        if not self.DynamicIncluded:
            raise NotImplementedError("Must provide a `DynamicIncluded` class")

        if kwargs.get('context', False):
            request = kwargs['context'].get('request', False)
            included = request.GET.get('include', False)
            if request and included:
                included_props = included.split(',')
                dynamic_fields = self.DynamicIncluded.__dict__
                for prop in included_props:
                    if dynamic_fields.get(prop, False):
self.fields[prop] = dynamic_fields.get(prop)```

and the usage looks like

```py
class MySerializer(serializer.Serializer):
  prop_one = serializer.CharField()

  def dynamic_method_field():
    return fibonnaci_sequence_n(1000)

  DynamicIncluded:
    fibonnanci = serizlizer.SerializerMethodField(method_name="dynamic_method_field")