Nested Serializer not showing up
Asked Answered
H

3

12

I am trying to update my serializers in my drf project to be shown in a nested way. The two models in question are Image and Gallery, images are related to Galleries.

I tried following https://www.django-rest-framework.org/api-guide/relations/#nested-relationships, but i am not entirely sure why it is not working.

Below is models.py

class Gallery(models.Model):
    title = models.CharField(max_length=30)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    created_on = models.DateTimeField(auto_now_add=True, blank=True)
    modified_on = models.DateTimeField(auto_now=True, blank=True)

    def __str__(self):
        return self.title


class Image(models.Model):
    gallery_id = models.ForeignKey(Gallery, on_delete=models.CASCADE)
    img = models.ImageField(upload_to='images/')
    created_on = models.DateTimeField(auto_now_add=True, blank=True)

serializers.py

class ImageSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Image
        fields = ["gallery_id", "img", "created_on", "id"]


class GallerySerializer(serializers.HyperlinkedModelSerializer):
    image = ImageSerializer(many=True, read_only=True)

    def validate(self, data):
        # Check if user id is equal object id before creation or if SuperUser
        request = self.context.get("request")
        if request.user.id != data["author"].id and request.user.is_superuser is not True:
            raise ValidationError("Unauthorized User Post")
        return data

    class Meta:
        model = Gallery
        fields = ["title", "author", "created_on", "modified_on", "image", "id"]

Expecting outcome would be

[
  {
    "title": "test_1",
    "author": "http://127.0.0.1:8000/api/users/2/",
    "created_on": "2019-08-19T09:13:45.107658Z",
    "modified_on": "2019-08-19T09:13:45.107731Z",
    "image": [
      {
        "gallery_id": "http://127.0.0.1:8000/api/galleries/24/",
        "img": "http://127.0.0.1:8000/media/images/angga-tantama-background-art-minimalism.jpg",
        "created_on": "2019-08-20T09:17:31.790901Z",
        "id": 6
      },
      {
        "gallery_id": "http://127.0.0.1:8000/api/galleries/24/",
        "img": "http://127.0.0.1:8000/media/images/art-vector-background-illustration-minimalism-angga-tantam-2.jpg",
        "created_on": "2019-08-20T09:31:40.505035Z",
        "id": 7
      }
    ]
    "id": 24
  },
  {
    "title": "test_2",
    "author": "http://127.0.0.1:8000/api/users/2/",
    "created_on": "2019-08-20T09:42:09.448974Z",
    "modified_on": "2019-08-20T09:42:09.449042Z",
    "id": 27
  }
]
Hadlee answered 20/8, 2019 at 9:50 Comment(1)
So what exactly is the error ?Terrilyn
E
35
image = ImageSerializer(many=True, read_only=True, source='image_set')

or

image_set = ImageSerializer(many=True, read_only=True)  # use image_set in fields list too.

Let's say you have a Gallery object similar to this:

g = Gallery.objects.get(pk=1)

Now the queryset for all the images related the given Galley object will be:

Image.objects.filter(gallery_id=g)  # g is the gallery object

In Django we can simplify it as:

g.image_set  # same as Image.objects.filter(gallery_id=g)

Now the thing is where does this magical image_set comes from. In Django ORM if you can use related_name in model's ForeignKey to query related objects, like this:

gallery_id = models.ForeignKey(Gallery, on_delete=models.CASCADE, related_name='something')
# if you do this in your models.py then to get all images of a gallery you will now do:
g.something

But since you didn't specified related_name in the ForeginKey it defaults to model name all lowercase + _set, therefore in this case: image_set.
Here is a link to docs.

If you specify two ForeignKey to same model from a model django will ask you to add related_name too (when making migrations), as it can default related name for one fields only.

This is called reverse_relationship. forward_relationship will be when you do something like this:

img = Image.objects.get(pk=1)
img.gallery_id  # get the gallery object related to the image

This is quite straightforward as gallery_id is a field in your model.


Also side note don't name your ForeignKey fields with a trailing _id, it's misleading. img.gallery_id is not the id of the Gallery it's the whole Gallery object. Django saves Related fields with a trailing id in the database, so in your case the column name in your database will be gallery_id_id (most likely, might have been changed in newer version).
When querying if your field name is gallery_id:
img.gallery_id  # the gallery object
img.gallery_id.id  # actual id of the gallery
img.gallery_id_id  # actual id of the gallery

But if you name your field just gallery:

img.gallery  # the gallery object
img.gallery.id  # actual id of the gallery
img.gallery_id  # actual id of the gallery

Which is a lot more clear.


So now you know why image_set is an attribute of your model class. And drf looks for model attributes in field names, so you either have your field name same the attribute(image_set) or specify the attribute using the source argument of the serializer.
Encyclical answered 20/8, 2019 at 9:52 Comment(0)
D
5

For things to work, you need to specify where Serializer should take data, using source keyword

In your case, this should do the trick.

class GallerySerializer(serializers.HyperlinkedModelSerializer):
    image = ImageSerializer(source="image_set", many=True, read_only=True)

    def validate(self, data):
        # Check if user id is equal object id before creation or if SuperUser
        request = self.context.get("request")
        if request.user.id != data["author"].id and request.user.is_superuser is not True:
            raise ValidationError("Unauthorized User Post")
        return data

    class Meta:
        model = Gallery
        fields = ["title", "author", "created_on", "modified_on", "image", "id"]

In the case where you used "related_name" in your FK declaration, you should use this reverse related name.

As you expected many Image instance, I strongly suggest you name your field images instead of image

I also suggest you have a look on permission instead of using getting your logic into your Serializers.

Dylandylana answered 20/8, 2019 at 9:54 Comment(0)
J
0

To contribute to this issue: if your serializer is a ModelSerializer or a HyperlinkedModelSerializer, and your nested serializer corresponds to a direct ForeignKey field (i.e. not an inverse relationship), a similar issue happens as the one discussed here: the name of the field where you put your nested serializer must be the same as in your model.

For example, if you have a model like this:

class Gallery(models.Model):
   title = models.CharField()
   author = models.ForeignKey(User, on_delete=models.CASCADE)
   ...

...and you want to access the ForeignKey field author via a nested serializer like this one (e.g. instead of accessing all the underlying model's fields straight, you want to access some of them via calculated fields):

class AuthorSerializer(serializers.HyperlinkedModelSerializer):
   class Meta:
      model = User
      fields = [ "username", "email", "fullname" ]
   fullname = serializers.SerializerMethodField()
   def get_fullname(self, entry):
      return entry.first_name + " " + entry.last_name

...then the top-level serializer where you put your nested serializer must have the same name as your ForeignKey field:

class GallerySerializer(serializers.HyperlinkedModelSerializer):
   class Meta:
      model = Gallery
      fields = [ "title", "author", ... ]
   author = AuthorSerializer()
   ...

Doing so will give you the data of the underlying foreign key as validated and formatted by the serializer. Failure to do so will result in the field not showing up in your serializer's output.

Judicial answered 13/1 at 21:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.