I'm trying to come up with an easy way to make my serializers and views work the way I want to, while preventing a bunch of manual boilerplate.
My simplified model:
class Character(models.Model):
name = models.CharField(max_length=255)
location = models.ForeignKey(Location, on_delete=models.SET_NULL, blank=True, null=True)
factions = models.ManyToManyField(Faction, blank=True)
So: character can have one location, and multiple factions.
Now, when I GET a character, I'd like those submodels (Location
and Faction
) to be expanded to their full representation, so my serializer looks like this:
class CharacterSerializer(serializers.ModelSerializer):
location = LocationSerializer(read_only=True)
factions = FactionSerializer(many=True, read_only=True)
class Meta:
model = Character
fields = "__all__"
And this works perfectly fine so far. The thing is, when I POST or PUT a character, I'd like to just send the id(s).. but still get the full location and faction objects back in the response.
In other words, when I create a character with a payload like this:
{"name":"Saga","location":1,"factions":[1]}
I'd like the response to look like this:
{
"id": 1,
"location": {
"id": 1,
"name": "Location 1"
},
"factions": [
{
"id": 1,
"name": "Faction 1"
}
],
"name": "Saga"
}
Is this possible at all without overriding the create
and update
methods of my ModelViewSet
subclass? I was hoping I could slightly modify the serializer itself to only apply those location
and factions
field serializers on the response, not on the request.
What I came up so far was to create my own ModelViewSet
subclass:
class DualSerializerModelViewSet(viewsets.ModelViewSet):
def create(self, request, *args, **kwargs):
write_serializer = self.write_serializer_class(data=request.data)
write_serializer.is_valid(raise_exception=True)
created_object = self.perform_create(write_serializer)
headers = self.get_success_headers(write_serializer.data)
read_serializer = self.serializer_class(created_object)
return Response(read_serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
instance = self.get_object()
write_serializer = self.write_serializer_class(instance, data=request.data, partial=partial)
write_serializer.is_valid(raise_exception=True)
self.perform_update(write_serializer)
if getattr(instance, "_prefetched_objects_cache", None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
read_serializer = self.serializer_class(instance)
return Response(read_serializer.data)
It can be used as such:
class WriteCharacterSerializer(serializers.ModelSerializer):
class Meta:
model = Character
fields = "__all__"
class CharacterSerializer(WriteCharacterSerializer):
location = LocationSerializer(read_only=True)
factions = FactionSerializer(many=True, read_only=True)
class CharacterController(DualSerializerModelViewSet):
serializer_class = CharacterSerializer
write_serializer_class = WriteCharacterSerializer
queryset = Character.objects.all()
This works pretty great for my use case, maybe it helps someone else as well.