Skip to content
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

Invalid self.instance when validating the serializer using many=True #8926

Open
sshishov opened this issue Apr 3, 2023 · 10 comments · Fixed by #8979
Open

Invalid self.instance when validating the serializer using many=True #8926

sshishov opened this issue Apr 3, 2023 · 10 comments · Fixed by #8979

Comments

@sshishov
Copy link
Contributor

sshishov commented Apr 3, 2023

Currently it is almost impossible to use the same serializer with and without many=True and the problem that sometimes serializer depending on the instance state or value during serialization and validation (usually it is PATCH method and partial=True

This is the simplest example to understand:

from django.db import models
from rest_framework import serializers


class MyClass(models.Model):
    name = models.CharField(max_length=100)
    status = models.CharField(max_length=100, blank=True)

    @property
    def is_valid(self):
        return self.name == 'valid'


class MyClassSerializer(serializers.ModelSerializer):
    class Meta:
        model = MyClass
        fields = ('id', 'name', 'status')

    def validate_status(self, value):
        if value and not self.instance.is_valid:
            raise serializers.ValidationError('Status cannot be set for invalid instance')
        return value


objs = MyClass.objects.bulk_create(
    objs=[
        MyClass(name='valid'),
        MyClass(name='invalid'),
        MyClass(name='other'),
    ]
)

serializer = MyClassSerializer(
    data=[{'status': 'set', 'id': instance.id} for instance in objs],
    instance=objs,
    many=True,
    partial=True,
)
serializer.is_valid()
print(serializer.errors)

Please note that it is just an example and real serializers and models can be much more complex. The case is working when we pass the single instance to the serializer but not working when we are trying to validate bulk operation, like update in bulk (let's say)

The following exception raised:

AttributeError: 'list' object has no attribute 'is_valid'

When expected behavior would be to get this one (pretty-printed):

[
    {},
    {'status': [ErrorDetail(string='Status cannot be set for invalid instance', code='invalid')]},
    {'status': [ErrorDetail(string='Status cannot be set for invalid instance', code='invalid')]},
]

P.S. I was able to manually patch the serializer to obtain the instance from the data provided, but I do not think that it should be done manually for every serializer. I assume that when we are validating the instance using PARTIAL, then the appropriate instance which is currently on validation, should be available inside the serializer. In this case we will be able to use the same serializer as single and multi entity-validation.

P.P.S. Also I think that if user require some logic for the list of objects, it should be placed inside the overridden list_serializer_class, no?

@saadullahaleem
Copy link
Contributor

I understand what you're trying to do here but I'm not sure how the values in the data list and the objs that you pass as the instance would correspond to each other. Would you assume that they are matched via indexes? Would you want to introduce a new keyword argument (e.g instances) just to make it clearer?

Also, from what I understand, many only works when you want serialize multiple objects into a list.

Interesting idea for a new feature, nonetheless. I know that Django does support bulk updating so this should be possible to do. However, I'm not sure if the team behind DRF would want to add this to the core project at this point.

@auvipy
Copy link
Member

auvipy commented May 3, 2023

if anything is supported in django ORM core then that should be supported in DRF too.

@saadullahaleem
Copy link
Contributor

My only concern is the mapping between the data and the objs in this example. Maybe instead of passing the objs it would be better to do something like this:

serializer = MyClassSerializer(
    data=[{'status': 'set', 'id': instance.id} for instance in objs],
    key='id',
    many=True,
    partial=True,
)

This way the serializer can internally make a query to to fetch all objects based on the provided key (the argument doesn't have to have this name) and make a single query to fetch them. It could then do a bulk update as defined in the Django Docs

@saadullahaleem
Copy link
Contributor

So I did some more digging into it. The problem is caused by the following method:

    def validate_status(self, value):
        if value and not self.instance.is_valid:
            raise serializers.ValidationError('Status cannot be set for invalid instance')
        return value

Specifically, it's caused by the self.instance is set to the value passed as data. Also, when you set many to True, the serializer that is declared is actually a ListSerializer instance. I'm not sure how we could make the self.instance variable point to the right dict object within the data list. From the looks of it, this would require significant reworking within the validation of the serializer classes.

@sshishov
Copy link
Contributor Author

@saadullahaleem it would be great to support it in a long term, but it is not a deal breaker currently (especially if we see the big redesign here)

@sshishov
Copy link
Contributor Author

sshishov commented Jun 3, 2023 via email

@auvipy
Copy link
Member

auvipy commented Jun 3, 2023

we will include this in a major release. so we can explicitly declare that

@sshishov
Copy link
Contributor Author

sshishov commented Jun 3, 2023

Will we be able to update stubs to support this? It will be a huge step forward...

Thank you @auvipy 👍

@auvipy
Copy link
Member

auvipy commented Dec 31, 2023

we might need a better implementation then the already merged PR

@auvipy
Copy link
Member

auvipy commented Mar 14, 2024

Have to reopen this as the initial fix was not enough to cover all related cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants