-
-
Notifications
You must be signed in to change notification settings - Fork 7k
Singe CRUD / request->response Operation two serializers #9630
-
G'Day,
Cant seem to find an answer anywhere, so hoping for an idea/solution.
- TL;DR. How to specify the serializer to use for the response. I wish to use one serializer for add/update and another for the JSON response within one CRUD operation / request-response.
Within my app, depending upon the situation I use different serializers. which serializer is used is dyno-magically determined by the viewsets logic. i.e. I have a view serializer for ALL get requests with the creation of the object being dynamic, serializer wise.
I have discovered recently that when creating an object, the correct serializer is used, however the serializer that is used for the JSON response is not the one I wish to use.
Looking at the code for the create of an object, It uses the same serializer for the create and the JSON return. (or have i looked in the wrong area?)
django-rest-framework/rest_framework/mixins.py
Lines 12 to 30 in 78e0b84
class CreateModelMixin:"""Create a model instance."""def create(self, request, *args, **kwargs):serializer = self.get_serializer(data=request.data)serializer.is_valid(raise_exception=True)self.perform_create(serializer)headers = self.get_success_headers(serializer.data)return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)def perform_create(self, serializer):serializer.save()def get_success_headers(self, data):try:return {'Location': str(data[api_settings.URL_FIELD_NAME])}except (TypeError, KeyError):return {}
Is this something I will be required to create a custom solution for? or is there a setting or process I have overlooked somewhere?
Beta Was this translation helpful? Give feedback.
All reactions
I had this problem a few times and there are a few ways to solve the problem, depending on how much your request schema differs from your response schema. Generally speaking, I think it's a good idea to minimise the differences. I'll describe a few options I've adopted over time.
Example
Let's use a simple BlogPost
model in an blogging application:
class BlogPost(models.Model): author = models.ForeignKey("auth.User", on_delete=models.CASCADE) title = models.CharField(max_length=255) body = models.TextField() created_at = models.DateTimeField(auto_now_add=True)
When a post of created, the author provides the title and body, and the other fields as derived from the request. ...
Replies: 1 comment 4 replies
-
I had this problem a few times and there are a few ways to solve the problem, depending on how much your request schema differs from your response schema. Generally speaking, I think it's a good idea to minimise the differences. I'll describe a few options I've adopted over time.
Example
Let's use a simple BlogPost
model in an blogging application:
class BlogPost(models.Model): author = models.ForeignKey("auth.User", on_delete=models.CASCADE) title = models.CharField(max_length=255) body = models.TextField() created_at = models.DateTimeField(auto_now_add=True)
When a post of created, the author provides the title and body, and the other fields as derived from the request. When reading a post, however, we want to display its author and creation date.
Solution 1: override the create method
With the solution you mention, the implementation might look like this:
# Serializers class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ("id", "first_name", "last_name", "username") class CreateBlogPostSerializer(serializers.ModelSerializer): """Serializer for creating posts.""" class Meta: model = BlogPost fields = ("title", "body") class BlogPostSerializer(CreateBlogPostSerializer): """Serializer for reading posts.""" author = UserSerializer() class Meta: model = BlogPost fields = ("id", "author", "title", "body", "created_at") # views class BlogPostViewSet(CreateModelMixin, RetrieveModelMixin, GenericViewSet): queryset = BlogPost.objects.all() serializer_class = BlogPostSerializer def create(self, request, *args, **kwargs): create_serializer = CreateBlogPostSerializer( data=request.data, context=self.get_serializer_context(), ) create_serializer.is_valid(raise_exception=True) post = create_serializer.save(author=self.request.user) response_serializer = self.get_serializer(post) headers = self.get_success_headers(response_serializer.data) return Response(response_serializer.data, status=status.HTTP_201_CREATED, headers=headers)
We override the create
method to add our customizations:
- Different serializer for request
- Derive the author from the request
This is however duplicating a fair bit of boilerplate from DRF, so not ideal.
Solution 2: override the get_serializer_class
method
A more chirurgical approach would be to override the get_serializer_class
method based on the HTTP verb:
class BlogPostViewSet(CreateModelMixin, RetrieveModelMixin, GenericViewSet): queryset = BlogPost.objects.all() serializer_class = BlogPostSerializer def get_serializer_class(self): if self.action == "create": return CreateBlogPostSerializer return super().get_serializer_class()
The problem however is that this serializer is used in both the request and the response of the create
action, so author
and created-at
would be omitted.
Solution 3: single serializer with read-only fields
Another solution is to specify which fields are read-only and use a single serializer:
class BlogPostSerializer(serializers.ModelSerializer): author = UserSerializer(read_only=True) class Meta: model = BlogPost read_only_fields = ("id", "author", "created_at") fields = read_only_fields + ("title", "body") class BlogPostViewSet(CreateModelMixin, RetrieveModelMixin, GenericViewSet): queryset = BlogPost.objects.all() serializer_class = BlogPostSerializer def perform_create(self, serializer): serializer.save(user=self.request.user)
Here the read-only fields are excluded from the request schema, but are included in the response schema. You can mix and match this approach with the previous one to bring CreateBlogPostSerializer
more in line with the read-only serializer.
Solution 4: have different fields in the request and response
Now imagine that our blog is divided into categories (each post belonging to a single one):
class Category(models.Model): title = models.CharField(max_length=255) description = models.TextField() class BlogPost(models.Model): ... # Same as before category = models.ForeignKey(Category, on_delete=models.CASCADE)
Categories are created ahead of time. When creating a post, we just want to pick one. When reading a post however, we want to get the whole category, with its title and description. We could modify our serializer like this:
class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category fields = ("id", "title", "description") class BlogPostSerializer(serializers.ModelSerializer): author = UserSerializer(read_only=True) category_id = serializers.PrimaryKeyRelatedField( queryset=Category.objects.all(), source="category", write_only=True, ) category = CategorySerializer(read_only=True) class Meta: model = BlogPost read_only_fields = ("id", "author", "created_at", "category") fields = read_only_fields + ("title", "body", "category_id")
Here is an explanation:
- Define a new
CategorySerializer
to get a nested representation - Add a write-only
category_id
field, to pass the primary key of the category only in the request - Add a read-only
category
field, to be used only on the response and return the whole serialized category, nested under our post.
Conclusion
Hopefully that gives you some pointers around how to achieve what you need. You may have to mix and matches some of these approaches. One thing that helped a lot for me is when the write_only
option clicked.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
wow, that's an answer. @browniebroke I really appreciate the time you have taken to assist with my dilemma.
I was already leaning towards solution 1
. Solution 2
wont work due to already customizing the serializer "get" functions within the views and the same serializer is still used in a single CRUD. Solution 3
wont work due to the use of the "response" serializer using nested serializers, unlike the request. Functionally solution 4
would work. Although solution 4
creates issues with drf_spectacular
auto docs features.
For completeness I should have included some context, example serialized objects. The "View" serializers use nested serializers for related models whilst the "create/update" serializers are based off of the model. For completeness, Here's the create serialized object.
{ "name": "string", "device_type": 0, "model_notes": "string", "serial_number": "string", "uuid": "string", "is_global": true, "is_virtual": true, "device_model": 0, "config": "string", "organization": 0 }
and the response / view serialized object (ALL get requests and the serializer to use in the CRUD response)
{ "id": 0, "status_icon": { "additionalProp1": "string", "additionalProp2": "string", "additionalProp3": "string" }, "display_name": "string", "name": "string", "device_type": { "id": 0, "display_name": "string", "name": "string", "url": "string" }, "model_notes": "string", "serial_number": "string", "uuid": "string", "is_global": true, "is_virtual": true, "device_model": { "id": 0, "display_name": "string", "name": "string", "url": "string" }, "config": "string", "rendered_config": "string", "inventorydate": "2025年01月25日T06:44:36.557Z", "context": { "additionalProp1": "string", "additionalProp2": "string", "additionalProp3": "string" }, "created": "2025年01月25日T06:44:36.557Z", "modified": "2025年01月25日T06:44:36.557Z", "organization": { "id": 0, "display_name": "string", "name": "string", "url": "string" }, "_urls": { "additionalProp1": "string", "additionalProp2": "string", "additionalProp3": "string" } }
Beta Was this translation helpful? Give feedback.
All reactions
-
🎉 1
-
Functionally
solution 4
would work. Althoughsolution 4
creates issues withdrf_spectacular
auto docs features.
I thought you might mention that, have you tried to use their extend_schema
decorator? It accepts a request
and responses
to pass your serializers to it:
https://drf-spectacular.readthedocs.io/en/latest/customization.html#step-2-extend-schema
It's super flexible and can do almost anything
Beta Was this translation helpful? Give feedback.
All reactions
-
Yeah am familiar with drf's extend_schema
. However using it, I'm not considering an option due to the additional work it creates.
Beta Was this translation helpful? Give feedback.
All reactions
-
Was causally browsing GitHub and stumbled on this.
I want to say - I typically have my own workarounds which mirror what is posted, nevertheless I am very glad that this is written here to validate how I usually go about solutioning my APIs.
Appreciate the hard work on this project 🫡
Beta Was this translation helpful? Give feedback.