Skip to content

Unexpected ValueError instead of ValidationError #74

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

Closed
penenkel opened this issue Sep 12, 2024 · 9 comments
Closed

Unexpected ValueError instead of ValidationError #74

penenkel opened this issue Sep 12, 2024 · 9 comments
Assignees
Labels
bug Something isn't working

Comments

@penenkel
Copy link

Given the following model

class MyModel(models.Model):
    class MyChoices(models.TextChoices):
        A = "a"
        B = "b"
        C = "c"

    my_field = EnumField(MyChoices, null=True, blank=True, default=None)

I would have expected MyModel(my_field="").full_clean() to throw a ValidationError, but instead it throws a ValueError in the coercion step.


Apart from that, kudos to you and your library, that you throw an error at all. Im quite happy that I have found it.

I was growing quite frustrated with the Django CharField based approach wrt. to nullable enum fields. It is infuriating that one is required to set blank=true to allow setting those fields to None/Null, which then automatically implies that an empty string is also allowed and it even sets default="" by default. I mean why?!

@bckohan
Copy link
Member

bckohan commented Sep 16, 2024

Thanks for the report! I'll look into it.

@bckohan
Copy link
Member

bckohan commented Sep 16, 2024

Can confirm this is a bug triggered by blank=True, if blank=False a validation error is raised instead

@bckohan bckohan self-assigned this Sep 16, 2024
@bckohan bckohan added the bug Something isn't working label Sep 16, 2024
@bckohan
Copy link
Member

bckohan commented Sep 16, 2024

Whats happening here is that Model.full_clean runs all validation routines on all fields and collects the validation errors and then at the end if there are any it throws a master ValidationError with all the collected validation errors. In this case a ValidationError is being thrown in the EnumField.clean method, but the model full_clean proceeds until it his the check constraint validations that eventually call get_db_prep_value which throws the ValueError during type coercion. This exception escapes full_clean causing the problem.

This will be a little tricky to correct without changing all errors to ValidationErrors - which may be ok. Its not clear to me if Model.get_db_prep_value and Model.get_prep_value are expected to raise ValidationErrors or not.

@bckohan
Copy link
Member

bckohan commented Sep 16, 2024

ok. I finally found some concrete guidance in the django docs:

For to_python(), if anything goes wrong during value conversion, you should raise a ValidationError exception.

@bckohan
Copy link
Member

bckohan commented Sep 16, 2024

Our to_python() function does honor this though. I don't see the harm in extending the ValidationErrors to the db_prep calls though. I will make the change.

There's no official documentation I can find about what get_db_prep_value and get_prep_value are supposed to raise, but most of the Django fields call to_python in those functions which is spec'ed to throw ValidationErrors. I think this change should be minimally invasive - I'll go ahead and make it and issue a 3.0.1 release.

@bckohan
Copy link
Member

bckohan commented Sep 16, 2024

However, can confirm that some django fields will happily throw a TypeError or ValueError out of get_prep_value. Look at IntegerField for example:

    def get_prep_value(self, value):
        value = super().get_prep_value(value)
        if value is None:
            return None
        try:
            return int(value)
        except (TypeError, ValueError) as e:
           raise e.__class__(
                "Field '%s' expected a number but got %r." % (self.name, value),
            ) from e

@bckohan
Copy link
Member

bckohan commented Sep 16, 2024

I'm now pretty convinced this is a bug in Django's CheckConstraint.validate. There seems to be no attempt to catch ValueError/TypeError and recast to ValidationError - which could result in 500s instead of 400s when full_clean() is invoked in certain paths when using IntegerField and CharField.

Instead of modifying EnumField.get_prep_value I'm going to subclass CheckConstraint and report this issue upstream.

@penenkel
Copy link
Author

I'm not quite sure if it is relevant here, but for the original Django CharField I figured out that .full_clean() acutally does not even try to validate an empty string if blank=True. Thats because it apperantly assumes all empty values to be valid in that case. And what an empty value is, is defined by the static attribute Field.empty_values. I managed to derive my own TextChoiceField from CharField and overwrote empty_values, now it is working as I want it to and declines empty strings because they are not part of the valid choices of my TextChoices type.

@bckohan
Copy link
Member

bckohan commented Sep 16, 2024

Thanks for the tip! That led me down a better path. I was trying to recreate this issue with non-blank invalid enums and couldnt because I do see now that full_clean doesnt run check constraint validation on those.

So yes this is very specific to stric enum character fields when blank=True and strict=True where the empty string is also not a valid enumeration value. I will update the empty_values list for fields in that specific case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants