diff options
| author | Tom Christie | 2014-10-22 10:32:32 +0100 | 
|---|---|---|
| committer | Tom Christie | 2014-10-22 10:32:32 +0100 | 
| commit | c5d1be8eac6cdb5cce000ec7c55e1847bfcf2359 (patch) | |
| tree | e15672da0e6e72dc2944108e27329a04e1e231bd | |
| parent | 05cbec9dd7f9f0b6a9b59b29ac6c9272b6ae50d8 (diff) | |
| download | django-rest-framework-c5d1be8eac6cdb5cce000ec7c55e1847bfcf2359.tar.bz2 | |
.validate() can raise field errors or non-field errors
| -rw-r--r-- | docs/topics/3.0-announcement.md | 45 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 34 | ||||
| -rw-r--r-- | tests/test_serializer.py | 26 | 
3 files changed, 94 insertions, 11 deletions
| diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index 658b50d3..9aeb5df6 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -163,7 +163,7 @@ The `validate_<field_name>` method hooks that can be attached to serializer clas              raise serializers.ValidationError('This field should be a multiple of ten.')          return attrs -This is now simplified slightly, and the method hooks simply take the value to be validated, and return it's validated value. +This is now simplified slightly, and the method hooks simply take the value to be validated, and return the validated value.      def validate_score(self, value):          if value % 10 != 0: @@ -172,6 +172,22 @@ This is now simplified slightly, and the method hooks simply take the value to b  Any ad-hoc validation that applies to more than one field should go in the `.validate(self, attrs)` method as usual. +Because `.validate_<field_name>` would previously accept the complete dictionary of attributes, it could be used to validate a field depending on the input in another field. Now if you need to do this you should use `.validate()` instead. + +You can either return `non_field_errors` from the validate method by raising a simple `ValidationError` + +    def validate(self, attrs): +        # serializer.errors == {'non_field_errors': ['A non field error']} +        raise serailizers.ValidationError('A non field error') + +Alternatively if you want the errors to be against a specific field, use a dictionary of when instantiating the `ValidationError`, like so: + +    def validate(self, attrs): +        # serializer.errors == {'my_field': ['A field error']} +        raise serailizers.ValidationError({'my_field': 'A field error'}) + +This ensures you can still write validation that compares all the input fields, but that marks the error against a particular field. +  #### Limitations of ModelSerializer validation.  This change also means that we no longer use the `.full_clean()` method on model instances, but instead perform all validation explicitly on the serializer. This gives a cleaner separation, and ensures that there's no automatic validation behavior on `ModelSerializer` classes that can't also be easily replicated on regular `Serializer` classes. @@ -189,7 +205,32 @@ REST framework 2.x attempted to automatically support writable nested serializat  * It's unclear what behavior the user should expect when related models are passed `None` data.  * It's unclear how the user should expect to-many relationships to handle updates, creations and deletions of multiple records. -Using the `depth` option on `ModelSerializer` will now create **read-only nested serializers** by default. To use writable nested serialization you'll want to declare a nested field on the serializer class, and write the `create()` and/or `update()` methods explicitly. +Using the `depth` option on `ModelSerializer` will now create **read-only nested serializers** by default. + +If you try to use a writable nested serializer without writing a custom `create()` and/or `update()` method you'll see an assertion error when you attempt to save the serializer. For example: + +    >>> class ProfileSerializer(serializers.ModelSerializer): +    >>>     class Meta: +    >>>         model = Profile +    >>>         fields = ('address', 'phone') +    >>> +    >>> class UserSerializer(serializers.ModelSerializer): +    >>>     profile = ProfileSerializer() +    >>>     class Meta: +    >>>         model = User +    >>>         fields = ('username', 'email', 'profile') +    >>> +    >>> data = { +    >>>     'username': 'lizzy', +    >>>     'email': 'lizzy@example.com', +    >>>     'profile': {'address': '123 Acacia Avenue', 'phone': '01273 100200'} +    >>> } +    >>> +    >>> serializer = UserSerializer(data=data) +    >>> serializer.save() +    AssertionError: The `.create()` method does not suport nested writable fields by default. Write an explicit `.create()` method for serializer `UserSerializer`, or set `read_only=True` on nested serializer fields. + +To use writable nested serialization you'll want to declare a nested field on the serializer class, and write the `create()` and/or `update()` methods explicitly.      class UserSerializer(serializers.ModelSerializer):          profile = ProfileSerializer() diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d29dc684..59f38a73 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -99,10 +99,10 @@ class BaseSerializer(Field):      def is_valid(self, raise_exception=False):          assert not hasattr(self, 'restore_object'), ( -            'Serializer %s has old-style version 2 `.restore_object()` ' +            'Serializer `%s.%s` has old-style version 2 `.restore_object()` '              'that is no longer compatible with REST framework 3. '              'Use the new-style `.create()` and `.update()` methods instead.' % -            self.__class__.__name__ +            (self.__class__.__module__, self.__class__.__name__)          )          if not hasattr(self, '_validated_data'): @@ -341,9 +341,22 @@ class Serializer(BaseSerializer):              value = self.validate(value)              assert value is not None, '.validate() should return the validated data'          except ValidationError as exc: -            raise ValidationError({ -                api_settings.NON_FIELD_ERRORS_KEY: exc.detail -            }) +            if isinstance(exc.detail, dict): +                # .validate() errors may be a dict, in which case, use +                # standard {key: list of values} style. +                raise ValidationError(dict([ +                    (key, value if isinstance(value, list) else [value]) +                    for key, value in exc.detail.items() +                ])) +            elif isinstance(exc.detail, list): +                raise ValidationError({ +                    api_settings.NON_FIELD_ERRORS_KEY: exc.detail +                }) +            else: +                raise ValidationError({ +                    api_settings.NON_FIELD_ERRORS_KEY: [exc.detail] +                }) +          return value      def to_internal_value(self, data): @@ -507,14 +520,17 @@ class ModelSerializer(Serializer):                  self._kwargs['validators'] = validators      def create(self, validated_attrs): +        # Check that the user isn't trying to handle a writable nested field. +        # If we don't do this explicitly they'd likely get a confusing +        # error at the point of calling `Model.objects.create()`.          assert not any(              isinstance(field, BaseSerializer) and not field.read_only              for field in self.fields.values()          ), (              'The `.create()` method does not suport nested writable fields '              'by default. Write an explicit `.create()` method for serializer ' -            '%s, or set `read_only=True` on nested serializer fields.' % -            self.__class__.__name__ +            '`%s.%s`, or set `read_only=True` on nested serializer fields.' % +            (self.__class__.__module__, self.__class__.__name__)          )          ModelClass = self.Meta.model @@ -544,8 +560,8 @@ class ModelSerializer(Serializer):          ), (              'The `.update()` method does not suport nested writable fields '              'by default. Write an explicit `.update()` method for serializer ' -            '%s, or set `read_only=True` on nested serializer fields.' % -            self.__class__.__name__ +            '`%s.%s`, or set `read_only=True` on nested serializer fields.' % +            (self.__class__.__module__, self.__class__.__name__)          )          for attr, value in validated_attrs.items(): diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 4df1b736..77d5c319 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -43,6 +43,32 @@ class TestSerializer:              serializer.data +class TestValidateMethod: +    def test_non_field_error_validate_method(self): +        class ExampleSerializer(serializers.Serializer): +            char = serializers.CharField() +            integer = serializers.IntegerField() + +            def validate(self, attrs): +                raise serializers.ValidationError('Non field error') + +        serializer = ExampleSerializer(data={'char': 'abc', 'integer': 123}) +        assert not serializer.is_valid() +        assert serializer.errors == {'non_field_errors': ['Non field error']} + +    def test_field_error_validate_method(self): +        class ExampleSerializer(serializers.Serializer): +            char = serializers.CharField() +            integer = serializers.IntegerField() + +            def validate(self, attrs): +                raise serializers.ValidationError({'char': 'Field error'}) + +        serializer = ExampleSerializer(data={'char': 'abc', 'integer': 123}) +        assert not serializer.is_valid() +        assert serializer.errors == {'char': ['Field error']} + +  class TestBaseSerializer:      def setup(self):          class ExampleSerializer(serializers.BaseSerializer): | 
