-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
Specifying different serializers for input and output #1563
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
Comments
Hi @foresmac, You've hit on one of the... erm... learning points of DRF. This sort of thing comes up on StackOverflow a number of times. Overriding If you fancy drafting up some changes in a pull request — or doing a blog post or something else like that — that would be cool. In the meantime I'll close this particular issue. |
Overriding |
@foresmac — I see — a slightly different case. I think the short answer is "Not automagically, not currently". I imagine the simplest thing (if this is really necessary) is setting the response data (with your output serialiser by hand. (But you've found your own way via
You really can get a long way with read only fields and so on — I can certainly believe there are cases where this isn't enough but I'm not sure at all that such cases would fall in the 80:20 that needs to be served (in the core) by DRF. If you think we're missing something, I recommend you explain it in depth, show where the code would change, show what use-cases would be resolved by it — if it sounds good, then open a pull request to that effect so that it can be reviewed. If you fancy taking a pop at Option 2 — that'll always be well received. |
Yeah, I'm fine either way, honestly. I just found it difficult to figure out how to do what I wanted from the docs, but as much may be my misunderstanding of how DRF is designed to work. Here's a sample so you can see what I am talking about; feel free to note if I'm doing something monumentally stupid and that there is/should be a better way in DRF itself that I'm just missing. from rest_framework import generics, status
from rest_framework.response import Response
from rack.models import RackItem
from rack.serializers import RackItemSerializer, NewRackItemSerializer
class ListCreateRackItem(generics.ListCreateAPIView):
model = RackItem
def get_serializer_class(self):
if self.request.method == 'POST':
return NewRackItemSerializer
return RackItemSerializer
def get_queryset(self):
return RackItem.objects.filter(shopper=self.request.user)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA)
if not serializer.is_valid():
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST)
item = RackItem.objects.create(
shopper=request.user,
item_url=serializer.data['item_url'],
item_image_url=serializer.data['item_image_url'])
result = RackItemSerializer(item)
return Response(result.data, status=status.HTTP_201_CREATED)
class GetUpdateDeleteRackItem(generics.RetrieveUpdateDestroyAPIView):
model = RackItem
serializer_class = RackItemSerializer
def get_queryset(self):
return RackItem.objects.filter(shopper=self.request.user) and the serializers themselves: from rest_framework import serializers
from models import RackItem
class RackItemSerializer(serializers.ModelSerializer):
class Meta:
model = RackItem
class NewRackItemSerializer(serializers.Serializer):
item_url = serializers.URLField()
item_image_url = serializers.URLField() |
The gist here is that I'm only getting some small bit of information to create a rack item; the server itself generates all the other fields on the model and stores them in the DB. But, I want my endpoint to spit out all that info when a new item is created. The pattern isn't too difficult, but all the docs seem to make a lot of assumptions that everyone is doing the 80% common case, and what's there makes it hard to see what to override or where to achieve other ends. If doing what I'm doing isn't common enough to address in the code, I'm more than happy to provide some sample code and an explanation of how it works and why. |
@foresmac — it looks to me like you're switching the serialisers in the most sensible way. However, I'd guess you could get the same result by marking the server-provided fields — |
There will be more fields that are editable later—mostly some boolean fields that record some user actions with the model—so I'm not sure that solves the problem in the long term. Agree that I probably should be making better use of I'm used to basically creating a Django form to use for input validation, and basically just building a |
The answer I came here to find turned out to be: If In this case, you'd use |
Im trying to tackle this issue right now. |
Your approach seems logical, why not just make a mixin out of it and re-use it. Something along the lines of:
|
I find myself in need of this as well, but in my case it's not for POST requests, but a GET request that returns a bunch of non-modal objects, and I need a bunch of filter parameters that are not directly related to the fields on the objects. I'm not sure if i'm doing something weird or missing something entirely, as I can't be the first to have an endpoint that is not model CRUD and needs to generate the documentation from the endpoint? I can just retrieve the arguments from the request object, but I'm trying to have my API be self-documenting by using the API documentation generator in drf, or the drf-yasg package, hence my reason for wanting to use the serializers for specifying the parameters. Sorry if this is not within the scope of this issue. |
Previously I said I was going to use https://github.com/vintasoftware/drf-rw-serializers. @Moulde Take a look at the lib Im using. If it does not suit you, try implementing the method |
@frenetic This seems to solve a problem that `get_serializer_class’ can already solve most of the time. What @Moulde (who presents a slightly different use-case than I previously mentioned) is saying is that there are times when you want/need to use different serializers for the Request vs the Response. And being able to take advantage of automatic documentation is an important consideration for this in my mind, outside of the desire to avoid boilerplate code in this case. |
Yes, basically I think DRF has less than ideal support for non-modal views, where you want to use a serializer to describe the interface, so that the automatic documentation can be generated. |
This was a big reason to use https://github.com/limdauto/drf_openapi (when it was still maintained). It would be awesome if it was possible to differentiate request and response schemas in django-rest-framework. There are a lot of times we're extending another api which has these characteristics. |
I do think that having In the absence of that, I wanted to highlight a couple current So a from django.db import models
class ThingModel(models.Model):
input_field = models.CharField()
output_date = models.DateTimeField()
output_id = models.IntegerField()
output_string = models.CharField() And a from rest_framework import serializers
from .models import ThingModel
class ThingSerializer(serializers.ModelSerializer):
class Meta:
model = ThingModel
fields = [
"input_field",
"output_date",
"output_id",
"output_string",
]
read_only_fields = [
"output_date",
"output_id",
"output_string",
]
extra_kwargs = {"input_field": {"write_only": True}} Will produce the following OpenAPI docs when ...
/urlpath/:
post:
operationId: CreateThing
parameters: []
requestBody:
content:
application/json:
schema:
properties:
input_field:
type: string
write_only: true
required:
- input_field
responses:
'200':
content:
application/json:
schema:
properties:
output_date:
type: string
format: date-time
readOnly: true
output_id:
type: integer
readOnly: true
output_string:
type: string
readOnly: true
required: []
description: '' This helps when you want one endpoint, within the context of a single method (eg |
What is the "state of the art" solution for this problem? Was there any progression since 2014? |
@fronbasal We've been accomplishing this with I'm not sure whether the maintainers have a different/better solution in mind |
@matthewwithanm All right, thank you very much! |
@fronbasal I have come up with a simple solution, just pass the model to another serializer here is the example def create(self, request):
serializer = InputSerializer(data=request.data)
output_serializer = serializer # default
if not serializer.is_valid():
return Response(
{'errors': serializer.errors},
status=status.HTTP_400_BAD_REQUEST)
try:
output_serializer = OutputSerializer(serializer.save())
except Exception as e:
raise ValidationError({"detail": e})
return Response(output_serializer.data, status=status.HTTP_201_CREATED) Cheers |
@michaelhenry thats a really clever solution! Love it. |
Thank you, just saved my day. I have the use case where i want to display more data of the owner of an object while retrieving it, but only give the owner's id while creating new entries. This was rather tedious to accomplish, because using nested serializers (even whith providing all of the required data) raises the following AssertionError:
So instead i had to overwrite See code for implementation detailsclass OwnerSerializer(serializers.Serializer):
"""Serializer for the related/nested owner information"""
id = serializers.UUIDField()
username = serializers.CharField()
def to_internal_value(self, data):
if not isinstance(data, str):
raise ValidationError("This value needs to be the owners UUID")
return User.objects.get(id=data)
class SomeModelSerializer(serializers.ModelSerializer):
"""The actual model with a relation to a user as owner"""
owner = OwnerSerializer()
class Meta:
model = SomeModel
fields = [
'title',
'owner',
...
] The request to create a new object can look like this: {
"title": "Some title",
"owner": "9c3faee7-2ce1-4b86-a58c-45f70a042125"
} While the response contains all desired nested information about the related owner after creation {
"title": "Some title",
"owner": {
"id": "9c3faee7-2ce1-4b86-a58c-45f70a042125",
"username": "some_user0815"
}
} |
I'd recommend an even cleaner way (for view layer):
|
Can we include the work done in People are adding a lot of workarounds here just to solve the issue with data and to generate proper OpenAPI schemes. |
I believe the above example is the best approach. For reusability, it's even better to wrap it up as CustomCreateModelMixin just to avoid repeating exactly the same code in all views where we need it. The ReadSerializer class can be obtained by temporarily changing self.action to 'retrieve' and calling get_serializer_class(). It's a hack but will work and will always make POST method return object formatted exactly as in the GET method. There are other things to consider as well, such as we may need to manually call get_queryset() to reload the object from DB before passing it to the output serializer, because our output serializer may expects some annotated fields or other custom stuff defined in view's get_queryset() |
@PawelRoman you do not encapsulate Currently we are using But as you mentioned it can be simplified:
To specify serializers, we can have:
Same way with dynamic approach, we need to override
What do you think guys? |
I find that, particularly when it comes to POST methods, that I often need a different serializer for input than for output. E.g., for a a particular model I may need only two or three input values, but the server will calculate/retrieve/whatever some additional values for fields on the model, and all those values need to get back to the client.
So far, my method has been to override
get_serializer_class()
to specify a separate input serializer for the request, and then overridecreate()
to use a different serializer for my output. That pattern works, but it took me some time to figure it out because the docs don't really suggest that option; the assumption that the generic APIViews are built around is that you specify one serializer and that is used for both input and output. I agree this generally works in the common case, but using my method breaks a little bit of the CBV magic. In particular, it can be difficult to troubleshoot if you make a mistake specifying a custom output serializer.I propose two solutions:
The text was updated successfully, but these errors were encountered: