ghazi-git / drf-standardized-errors

Standardize your DRF API error responses
https://drf-standardized-errors.readthedocs.io/en/latest/
MIT License
257 stars 15 forks source link

Having a hard time trying to add custom validation in 400 errors #50

Closed GabrielLins64 closed 11 months ago

GabrielLins64 commented 11 months ago

Hi, I'm using

djangorestframework==3.14.0
djangorestframework-simplejwt==5.3.0
drf-spectacular==0.26.5
drf-standardized-errors==0.12.6

On my CustomUserSerializer(serializers.ModelSerializer) I have the following username validator:

        extra_kwargs = {
            'username': {'validators': [validate_cpf,]},
        }

where

def validate_cpf(value):
    if not is_valid_cpf(value):
        raise ValidationError('O nome de usuário precisa ser um CPF válido.')

I actually get the correct response when sendind a wrong payload via postman:

{
    "type": "validation_error",
    "errors": [
        {
            "code": "invalid",
            "detail": "O nome de usuário precisa ser um CPF válido.",
            "attr": "username"
        }
    ]
}

However, the swagger schema seems to fail in finding the validation for this code:

{
  "errors": [
    {
      "attr": "non_field_errors",
      "code": "invalid",
      "detail": "string"
    },
    {
      "attr": "username",
      "code": "blank",
      "detail": "string"
    },
    {
      "attr": "password",
      "code": "blank",
      "detail": "string"
    },
    ...
  ]
}

I've even tried using the DRF-way to defining validation errors, as mentioned in the docs:

class CustomUserSerializer(serializers.ModelSerializer):
    default_error_messages = {"invalid_username": "O nome de usuário precisa ser um CPF válido."}

    def validate_username(self, username):
        if not is_valid_cpf(username):
            self.fail('invalid_username')
        return username

But this also doesn't work. Am I missing something?

ghazi-git commented 11 months ago

Hi @GabrielLins64,

On my CustomUserSerializer(serializers.ModelSerializer) I have the following username validator:

        extra_kwargs = {
            'username': {'validators': [validate_cpf,]},
        }

where

def validate_cpf(value):
    if not is_valid_cpf(value):
        raise ValidationError('O nome de usuário precisa ser um CPF válido.')

in the above, the package will not find the error code because it only checks the error_messages of the serializer and its fields.

I've even tried using the DRF-way to defining validation errors, as mentioned in the docs:

class CustomUserSerializer(serializers.ModelSerializer):
    default_error_messages = {"invalid_username": "O nome de usuário precisa ser um CPF válido."}

    def validate_username(self, username):
        if not is_valid_cpf(username):
            self.fail('invalid_username')
        return username

In this case, I would have expected to see the code in the API schema with attr as non_field_error because it was added to the default_error_messages of the serializer.

...
    {
      "attr": "non_field_errors",
      "code": "invalid_username",
      "detail": "string"
    },
...

So, can you please provide a minimal example: code (url, view, serializer) + the resulting API schema so I can investigate this more when I get some free time?

Also, check out the decorator extend_validation_errors since it allows adding error codes to specific fields

@extend_validation_errors(["invalid_username"], field_name="username", actions=["create", "update"])
class UserViewSet(ModelViewSet):
    ...
GabrielLins64 commented 11 months ago

Hi @ghazi-git , thank you for the fast reply!

I just tried the @extend_validation_errors decorator and it worked for the API schema, but not for the "Example value" (screenshots attached by the end of the msg).

This is the updated validator:

def validate_cpf(value):
    if not is_valid_cpf(value):
        raise ValidationError(detail='O nome de usuário precisa ser um CPF válido.', code='invalid_cpf')

The serializer:

class CustomUserSerializer(serializers.ModelSerializer):
    groups = GroupSerializer(many=True, read_only=True)

    class Meta:
        model = CustomUser
        fields = [
            'id',
            'username',
            'password',
            'first_name',
            'last_name',
            'email',
            'phone',
            'is_staff',
            'is_superuser',
            'created_at',
            'groups',
        ]
        extra_kwargs = {
            'id': {'read_only': True},
            'created_at': {'read_only': True},
            'username': {'validators': [validate_cpf,]},
            'is_staff': {'read_only': True},
            'is_superuser': {'read_only': True},
            'password': {'write_only': True},
        }

    # def validate_username(self, username):
    #     if not is_valid_cpf(username):
    #         raise serializers.ValidationError('O nome de usuário precisa ser um CPF válido.', code='invalid_cpf')
    #     return username

    def validate_password(self, password):
        if not is_valid_password(password):
            raise serializers.ValidationError(
                'A senha precisa ter no mínimo 8 caracteres, pelo meno 1 dígito, uma letra maiúscula, uma letra minúscula e um caractere especial (#?!@$%^&*-).')
        return password

    def create(self, validated_data):
        user = super().create(validated_data)
        user.set_password(validated_data['password'])
        user.save()
        return user

The view:

from rest_framework.generics import CreateAPIView

@extend_validation_errors(["invalid_cpf"], field_name="username", methods=['post'])
class CustomUserCreateView(CreateAPIView):
    serializer_class = CustomUserSerializer
    permission_classes = [IsAdminOrReadOnly]

The url:

urlpatterns = [
    path('', CustomUserCreateView.as_view(), name='user-create'),
]

This is the resulting API schema: image It did saw the invalid_cpf code, but the "Example value" doesn't bring it nor the specified error detail (I'm not sure if is this the expected behavior): image

ghazi-git commented 11 months ago

About the examples, that is the expected behavior: the package does not provide examples for 400 error responses due to their dynamic nature (though it does provide examples for other error responses 401, 403, ...). So, what's happening is that swagger UI is automatically generating examples when they are not provided (for each attr, it's showing an example with the first error code).

About the error code not showing up: taking your code, removing the @extend_validation_errors decorator from the view and adding a default_error_messages attribute to the serializer results in the error code showing up in swagger UI. The addition of the error code to the default_error_messages is required for it to show up.

class CustomUserSerializer(serializers.ModelSerializer):
    default_error_messages = {"invalid_cpf": "O nome de usuário precisa ser um CPF válido."}
    ...

image

GabrielLins64 commented 11 months ago

Oh, ok. Thank you!