Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Singe CRUD / request->response Operation two serializers #9630

Discussion options

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?)

  • 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?

You must be logged in to vote

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

Comment options

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.

You must be logged in to vote
4 replies
Comment options

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"
 }
}
Comment options

Functionally solution 4 would work. Although solution 4 creates issues with drf_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

Comment options

Yeah am familiar with drf's extend_schema. However using it, I'm not considering an option due to the additional work it creates.

Comment options

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 🫡

Answer selected by jon-nfc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /