[Django]-Django rest framework hide specific fields in list display?

22👍

For anyone else stumbling across this, I found overriding get_serializer_class on the viewset and defining a serializer per action was the DRY-est option (keeping a single viewset but allowing for dynamic serializer choice):

class MyViewset(viewsets.ModelViewSet):
    serializer_class = serializers.ListSerializer
    permission_classes = [permissions.IsAdminUser]
    renderer_classes = (renderers.AdminRenderer,)
    queryset = models.MyModel.objects.all().order_by('-updated')

    def __init__(self, *args, **kwargs):
        super(MyViewset, self).__init__(*args, **kwargs)
        self.serializer_action_classes = {
            'list':serializers.AdminListSerializer,
            'create':serializers.AdminCreateSerializer,
            'retrieve':serializers.AdminRetrieveSerializer,
            'update':serializers.AdminUpdateSerializer,
            'partial_update':serializers.AdminUpdateSerializer,
            'destroy':serializers.AdminRetrieveSerializer,
        }

    def get_serializer_class(self, *args, **kwargs):
        """Instantiate the list of serializers per action from class attribute (must be defined)."""
        kwargs['partial'] = True
        try:
            return self.serializer_action_classes[self.action]
        except (KeyError, AttributeError):
            return super(MyViewset, self).get_serializer_class()

Hope this helps someone else.

👤Liz

10👍

You can override the ‘get_fields’ method your serializer class and to add something like that:

def get_fields(self, *args, **kwargs):
    fields = super().get_fields(*args, **kwargs)
    request = self.context.get('request')
    if request is not None and not request.parser_context.get('kwargs'):
        fields.pop('your_field', None)
    return fields

In this case when you get detail-view there is ‘kwargs’: {‘pk’: 404} and when you get list-view there is ‘kwargs’: {}

👤Sergei

9👍

I wrote an extension called drf-action-serializer (pypi) that adds a serializer called ModelActionSerializer that allows you to define fields/exclude/extra_kwargs on a per-action basis (while still having the normal fields/exclude/extra_kwargs to fall back on).

The implementation is nice because you don’t have to override your ViewSet get_serializer method because you’re only using a single serializer. The relevant change is that in the get_fields and get_extra_kwargs methods of the serializer, it inspects the view action and if that action is present in the Meta.action_fields dictionary, then it uses that configuration rather than the Meta.fields property.

In your example, you would do this:

from action_serializer import ModelActionSerializer

class PersonSerializer(ModelActionSerializer):
    class Meta:
        model = Person
        fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')
        action_fields = {
            'list': {'fields': ('nickname', 'slug')}
        }

Your ViewSet would look something like:

class PersonViewSet(viewsets.ModelViewSet):
    queryset = Person.objects.all()
    serializer_class = PersonSerializer

And your router would look normal, too:

router = routers.DefaultRouter()
router.register(r'persons', api.PersonViewSet)

Implementation

If you’re curious how I implemented this:

  1. I added a helper method called get_action_config which gets the current view action and returns that entry in the action_fields dict:
def get_action_config(self):
    """
    Return the configuration in the `Meta.action_fields` dictionary for this
    view's action.
    """
    view = getattr(self, 'context', {}).get('view', None)
    action = getattr(view, 'action', None)
    action_fields = getattr(self.Meta, 'action_fields', {})
  1. I changed get_field_names of ModelSerializer:

From:

fields = getattr(self.Meta, 'fields', None)
exclude = getattr(self.Meta, 'exclude', None)

To:

action_config = self.get_action_config()
if action_config:
    fields = action_config.get('fields', None)
    exclude = action_config.get('exclude', None)
else:
    fields = getattr(self.Meta, 'fields', None)
    exclude = getattr(self.Meta, 'exclude', None)
  1. Finally, I changed the get_extra_kwargs method:

From:

extra_kwargs = copy.deepcopy(getattr(self.Meta, 'extra_kwargs', {}))

To:

action_config = self.get_action_config()
if action_config:
    extra_kwargs = copy.deepcopy(action_config.get('extra_kwargs', {}))
else:
    extra_kwargs = copy.deepcopy(getattr(self.Meta, 'extra_kwargs', {}))

2👍

If you want to change what fields are displayed in the List vs Detail view, the only thing you can do is change the Serializer used. There’s no field that I know of that lets you specify which fields of the Serializer gets used.

1👍

The field selection on you serializers should be working, but I don’t know what might be happening exactly. I have two solutions you can try:

1 Try to change the way you declare you serializer object

#If you aren't using Response: 
from rest_framework.response import Response

class PersonListViewSet(viewsets.ModelViewSet):
    def get(self, request):
       queryset = Person.objects.all()
       serializer_class = PersonListSerializer(queryset, many=True) #It may change the things
       return Response(serializer_class.data)


class PersonViewSet(viewsets.ModelViewSet):
    def get(self, request, pk): #specify the method is cool
       queryset = Person.objects.all()
       serializer_class = PersonSerializer(queryset, many=True) #Here as well
       #return Response(serializer_class.data)

2 The second way around would change your serializers

This is not the most normal way, since the field selector should be working but you can try:

class PersonListSerializer(serializers.ModelSerializer):
    nickname = serializers.SerializerMethodField() #Will get the attribute my the var name
    slug = serializers.SerializerMethodField()
    class Meta:
        model = Person
    def get_nickname(self, person): 
        #This kind of method should be like get_<fieldYouWantToGet>() 
        return person.nickname
    def get_slug(self, person):
        #This kind of method should be like get_<fieldYouWantToGet>() 
        return person.slug

I hope it helps. Try to see the APIview class for building your view too.

0👍

Somehow close:
If you just want to skip fields in the serilaizer

class UserSerializer(serializers.ModelSerializer):
    
    user_messages = serializers.SerializerMethodField()
    def get_user_messages(self, obj):
        
        if self.context.get('request').user != obj: 
            # do somthing here check any value from the request:
            # skip others msg
            return
        # continue with your code 
        return SystemMessageController.objects.filter(user=obj, read=False)

0👍

I rewrite ModelViewSet list function to modify serializer_class.Meta.fields attribute, code like this:

class ArticleBaseViewSet(BaseViewSet):
    def list(self, request, *args, **kwargs):
        exclude = ["content"]
        self.serializer_class.Meta.fields = [f.name for f in self.serializer_class.Meta.model._meta.fields if f.name not in exclude]
        queryset = self.filter_queryset(self.get_queryset()).filter(is_show=True, is_check=True)

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

class BannerArticleViewSet(ArticleBaseViewSet):
    queryset = BannerArticle.objects.filter(is_show=True, is_check=True).all()
    serializer_class = BannerArticleSerializer
    permission_classes = (permissions.AllowAny,)

But it looks not stable, so i will not use it, just share to figure out the best way

0👍

My solution.

class BaseSerializerMixin(_ModelSerializer):
    class Meta:
        exclude: tuple[str, ...] = ()
        exclude_in_list: tuple[str, ...] = ()
        model: Type[_models.Model]

    def get_action(self) -> Optional[str]:
        if 'request' not in self.context:
            return None
        return self.context['request'].parser_context['view'].action

    def get_fields(self):
        fields = super().get_fields()
        if self.get_action() == 'list':
            [fields.pop(i) for i in list(fields) if i in self.Meta.exclude_in_list]
        return fields

-1👍

I think it should be like this:

router.register(r'persons/?P<slug>/', api.PersonViewSet)

and you should include a line like this:

lookup_field='slug'

in your serializer class. Like this:

class PersonSerializer(serializers.ModelSerializer):
    lookup_field='slug'
    class Meta:
        model = Person
        fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')

Leave a comment