Skip to content

[Feature request] Different serializers for input and output approach #8181

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
LeOndaz opened this issue Sep 23, 2021 · 9 comments
Closed

[Feature request] Different serializers for input and output approach #8181

LeOndaz opened this issue Sep 23, 2021 · 9 comments

Comments

@LeOndaz
Copy link

LeOndaz commented Sep 23, 2021

Hello.

First, I've read the contribution guide over and over and I read this:

Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintenance overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation.

over and over before submitting this, but bear in mind. I'll explain with a usage example.

Second: This is more of a discussion to see your thoughts, I currently don't have the solution for this specific thing, I will just show my solutions for the sake of showing the problem, rather than solving it.

I have an Issue related to Task, so every task has some issues and an issue is related to one task, it's modeled in django and everything is fine regarding this. However, let's model it in DRF.

class TaskSerializer(serializers.ModelSerializer):
    """
    There're lots of this x = XSerializer() inside TaskSerializer but I removed for simplicity,
    it has more than 10 fields that need to be serialized.
    """
    class Meta:
        model = Task
        exclude = []
class IssueSerializer(serializers.ModelSerializer):
    task = TaskSerializer()
    assigned = UserSerializer()

    class Meta:
        model = Issue
        exclude = []

Let's think about something, many people opened issues and asked on stackoverflow about different serializers for "Input" and "Output" rather than "different request methods" or "actions", right?

Postman image that has an example request

I'm trying to create a new issue, and the issue has a task field, I want to mention the task with it's ID when I'm talking to the API, BUT I want the API to serialize the task when it's talking to me. I did the logic as in the screenshot, but I want to see if this is a behavior that we can have directly in DRF because it's the most reasonable thing to do instead of responding with PKs and requesting the data with that pk, ..etc, saving lots of trips between server and client(s).

All of this happened in a POST request and the action is still create. This is only typed as:

class IssueSerializer(serializers.ModelSerializer):
    task = PrimaryKeyRelatedField(repr_serializer=TaskSerializer, queryset=Task.objects.all())
    assigned = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), write_only=True)

    class Meta:
        model = Issue
        exclude = []

and I added a field repr_serializer to serialize the PKOnlyObject to the object of type queryset.model included in the initializer of the Field.

What do you think?

@LeOndaz
Copy link
Author

LeOndaz commented Sep 23, 2021

The current problem is that pk_field expects a callable thatt takes the pk, but I'm trying to see if we can make it work with the serializers as TaskSerializer without having to edit anything or create a different serializer to only get the object via the id or smth

@LeOndaz
Copy link
Author

LeOndaz commented Sep 23, 2021

I've implemented something to put in pk_field, but it's too much verbose to be typed each time.

class PKField(serializers.Serializer):
    def __init__(self, serializer_class, **kwargs):
        self.serializer_class = serializer_class
        super().__init__(**kwargs)

    def get_object(self, pk):
        ModelClass = self.serializer_class.Meta.model  # noqa
        return ModelClass.objects.get(pk=pk)

    def to_representation(self, value):
        instance = self.get_object(value)
        return self.serializer_class(instance).data

    def to_internal_value(self, data):
        return data

and

    task = serializers.PrimaryKeyRelatedField(pk_field=PKField(serializer_class=TaskSerializer), queryset=Task.objects.all())

And it will work, takes in ID when the data is being mentioned (created, updated), but returns the object dict when it's getting represented.

Maybe a custom field will also do it, a custom PrimaryKeyRelatedField, but that's still too much for something that gets typed in many projects

What do you think?

@cdelacombaz
Copy link

cdelacombaz commented Sep 29, 2021

Hi LeOndaz

This might be exactly what you need: https://deladev.ch/posts/drf-serializer-with-different-input-output

Sorry for my poorly formatted blog without syntax highlighting, it's work in progress :P

@LeOndaz
Copy link
Author

LeOndaz commented Sep 29, 2021

Hi LeOndaz

This might be exactly what you need: https://deladev.ch/posts/drf-serializer-with-different-input-output

Sorry for my poorly formatted blog without syntax highlighting, it's work in progress :P

Hello. Thank you for your effort, I'm aware of many solutions for this specific issue, I'm just curious if there're any intentions to add a builtin solution in DRF

@Smixi
Copy link

Smixi commented Dec 25, 2021

Hi !
I have done something like that on my project, by using modifying each mixins :

class LendListModelMixin:
    """
    List a queryset.
    """
    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())
        page = self.paginate_queryset(queryset)
        if page is not None:
            output_serializer = self.get_output_serializer(page, many=True)
            return self.get_paginated_response(output_serializer .data)

        output_serializer = self.get_output_serializer(queryset, many=True)
        return Response(output_serializer.data)

and for the base view :

class LendGenericViewSet(GenericViewSet):

    output_serializer_class = None

    def get_output_serializer(self, *args, **kwargs):
        """
        Return the output serializer instance that should be used for serializing output.
        """
        serializer_class = self.get_output_serializer_class()
        kwargs.setdefault('context', self.get_serializer_context())
        return serializer_class(*args, **kwargs)
    
    def get_output_serializer_class(self): 
        """
        Return the class to use for the output serializer.
        Defaults to using `self.serializer_class`.

        You may want to override this if you need to provide different
        serializations depending on the incoming request.

        (Eg. admins get full serialization, others get basic serialization)
        """
        assert self.output_serializer_class is not None, (
            "'%s' should either include a `output_serializer_class` attribute, "
            "or override the `get_output_serializer_class()` method."
            % self.__class__.__name__
        )

        return self.output_serializer_class

I also added support for it on the schema of drf-spectacular :

class LendSchema(AutoSchema):
    def get_response_serializers(self) -> typing.Any:
        view = self.view
        if isinstance(view, LendModelViewSet):
        # try to circumvent queryset issues with calling get_serializer. if view has NOT
            # overridden get_serializer, its safe to use get_serializer_class.
            if view.__class__.get_serializer == GenericAPIView.get_serializer:
                return view.get_output_serializer_class()
            return view.get_serializer()
        else:
            return super().get_response_serializers()

but i guess it would be similar to the schema generator of drf. Would you be interested into me making a PR ?

@Smixi
Copy link

Smixi commented Jan 31, 2022

@khamaileon is this #8347 equivalent ?

@khamaileon
Copy link

It looks like the same idea. Except that the schema part already exists in commit 812394ed83d7cce0ed5b2c5fcf093269d364b9b.

@Smixi
Copy link

Smixi commented Jan 31, 2022

Well yeah, it provides this functionality accross all generics, quite handy ! I guess it then integrate perfectly with DRF-Spectacular then, I just missed this features and realized it was already there only now, thank you 👍 .

@stale
Copy link

stale bot commented Apr 3, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Apr 3, 2022
@tomchristie tomchristie reopened this Jun 6, 2022
@stale stale bot removed the stale label Jun 6, 2022
@tomchristie tomchristie closed this as not planned Won't fix, can't repro, duplicate, stale Jun 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants