Dynamically exclude or include a field in Django REST framework serializer
Asked Answered
B

6

44

I have a serializer in Django REST framework defined as follows:

class QuestionSerializer(serializers.Serializer):
    id = serializers.CharField()
    question_text = QuestionTextSerializer()
    topic = TopicSerializer()

Now I have two API views that use the above serializer:

class QuestionWithTopicView(generics.RetrieveAPIView):
    # I wish to include all three fields - id, question_text
    # and topic in this API.
    serializer_class = QuestionSerializer

class QuestionWithoutTopicView(generics.RetrieveAPIView):
    # I want to exclude topic in this API.
    serializer_class = ExamHistorySerializer

One solution is to write two different serializers. But there must be a easier solution to conditionally exclude a field from a given serializer.

Bombardier answered 14/1, 2015 at 3:47 Comment(0)
G
69

Have you tried this technique

class QuestionSerializer(serializers.Serializer):
    def __init__(self, *args, **kwargs):
        remove_fields = kwargs.pop('remove_fields', None)
        super(QuestionSerializer, self).__init__(*args, **kwargs)

        if remove_fields:
            # for multiple fields in a list
            for field_name in remove_fields:
                self.fields.pop(field_name)

class QuestionWithoutTopicView(generics.RetrieveAPIView):
        serializer_class = QuestionSerializer(remove_fields=['field_to_remove1' 'field_to_remove2'])

If not, once try it.

Gulgee answered 14/1, 2015 at 4:50 Comment(6)
This should work. But I was expecting something to be built into Django rest framework. I would use this if I don't find any cleaner solution.Bombardier
I would suggest you correct if conditions as if remove_fields and not getattr(self, 'many', False):Leverage
It should be noted that this only works for read only serializers. This will break on create or update on serializers that perform CRUD operations.Lanford
@SudipKafle Similar method suggested in DRF's documentation also django-rest-framework.org/api-guide/serializers/…Tinaret
Using a SerializerMethodField for one of the fields that I have included in my serializer and am getting an error if I try to remove it - 'field' was declared on the serializer but not included in the fields option.Circuit
Be careful with this approach, it does NOT work for many=TrueLamphere
L
24

Creating a new serializer is the way to go. By conditionally removing fields in a serializer you are adding extra complexity and making you code harder to quickly diagnose. You should try to avoid mixing the responsibilities of a single class.

Following basic object oriented design principles is the way to go.

QuestionWithTopicView is a QuestionWithoutTopicView but with an additional field.

class QuestionSerializer(serializers.Serializer):
        id = serializers.CharField()
        question_text = QuestionTextSerializer()
        topic = TopicSerializer()

class TopicQuestionSerializer(QuestionSerializer):
       topic = TopicSerializer()
Lanford answered 21/7, 2015 at 2:37 Comment(6)
It is not always the case that conditionally removing fields is incorrect. If i have 100 models which have 10 fields each i want to never display in the serializer, except in some cases, i'll have to manually exclude a thousand fields. I'd rather exlude them automagically and create a one or two serperate serializers who do add those fields for those exceptional cases.Trimly
In your example you would not have to manually exclude a thousand fields. If you have 100 models which have 10 field each you never want to display in the serializer except in some cases then create a serializer that has all the fields in it. Then create a second serializer that extends the first and uses the exclude_fields Meta class option.Lanford
Also note you should probably be using ModelSerializers and not constructing you own serializers if thats the case.Lanford
Indeed, you're right. I was thinking ahead of myself. My use case is a little less straight forward than only the models with excluded fields. In my project i have user defined dynamic models too. In which the user should be able to add fields to the database, which will be excluded without having to modify the source code. I was thinking besides those lines, projecting them on this case ;)Trimly
I see how overriding the get_serializer_class is appealing if you have a simple case. But I have a ModelSerializer that has a handful of fields, 2 of which are each conditionally allowed depending on the user's access (location, `price1). Which this method I would need 4 classes to cover the 4 possible permutations (Can see all, can see neither, can see location, can see price). Is there a nice way I can accomplish this in get_serializer_class?Gadid
In my opinion it is acceptable to dynamically set and remove fields. It does add some complexity to the serializers classes themselves, but reduces complexity by removing a bunch of semi-duplicate serializers. We use a default function set_fields and set_exclude_fields to each serializer.Policy
F
3

You can set fields and exclude properties of Meta

Here is an Example:

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        exclude = ['id', 'email', 'mobile']

    def __init__(self, *args, **kwargs):
        super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
        #  @note: For example based on user,  we will send different fields
        if self.context['request'].user == self.instance.user:
            # Or set self.Meta.fields = ['first_name', 'last_name', 'email', 'mobile',]
            self.Meta.exclude = ['id']

Farmyard answered 13/7, 2021 at 13:34 Comment(0)
B
2

Extending above answer to a more generic one

class QuestionSerializer(serializers.Serializer):
    def __init__(self, *args, **kwargs):
        fields = kwargs.pop('fields', None)
        super(QuestionSerializer, self).__init__(*args, **kwargs)
        if fields is not None:
            allowed = set(fields.split(','))
            existing = set(self.fields)
            for field_name in existing - allowed:
                self.fields.pop(field_name)

class QuestionWithoutTopicView(generics.RetrieveAPIView):
    def get_serializer(self, *args, **kwargs):
        kwargs['context'] = self.get_serializer_context()
        fields = self.request.GET.get('display')
        serializer_class = self.get_serializer_class()
        return serializer_class(fields=fields,*args, **kwargs)
    def get_serializer_class(self):
        return QuestionSerializer
    

Now we can give a query parameter called display to output any custom display format http://localhost:8000/questions?display=param1,param2

Barred answered 1/6, 2021 at 18:9 Comment(0)
I
0

You can use to representation method and just pop values:

def to_representation(self, instance):
    """Convert `username` to lowercase."""
    ret = super().to_representation(instance)
    ret.pop('username') = ret['username'].lower()
    return ret

you can find them here

https://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior

Indoors answered 26/12, 2022 at 13:35 Comment(0)
P
0

We add a set_fields and set_exclude_fields to each serializer so that they are easily customizable. Initially we created a new serializer for each API, but this let to a wildgrowth of serializers.

It looks like this:

class YourModelSerializer(serializers.ModelSerializer):

    class Meta:
        model = YourModel
        fields = ("__all__")

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

Then in a util file for shared functions among serializers we have these two functions:

def set_fields(serializer, kwargs):
    fields = kwargs.pop("fields", None)
    if fields:
        serializer_fields = {field_name: serializer.fields[field_name] for field_name in fields if field_name in serializer.fields}
        serializer.fields = serializer_fields

def set_exclude_fields(serializer, kwargs):
    exclude_fields = kwargs.pop("exclude_fields", None)
    if exclude_fields:
        for field_name in exclude_fields:
            serializer.fields.pop(field_name, None)

You call it like this:

serializer = YourModelSerializer(fields=["field"], exclude_fields=["field"])

Keep in mind though that this could lead to unpredictable outputs of your APIs if you always customize the fields. That could be problematic for the frontend.

Policy answered 24/1 at 10:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.