.validate() can raise field errors or non-field errors

This commit is contained in:
Tom Christie 2014-10-22 10:32:32 +01:00
parent 05cbec9dd7
commit c5d1be8eac
3 changed files with 94 additions and 11 deletions

View File

@ -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.') raise serializers.ValidationError('This field should be a multiple of ten.')
return attrs 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): def validate_score(self, value):
if value % 10 != 0: 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. 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. #### 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. 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 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. * 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): class UserSerializer(serializers.ModelSerializer):
profile = ProfileSerializer() profile = ProfileSerializer()

View File

@ -99,10 +99,10 @@ class BaseSerializer(Field):
def is_valid(self, raise_exception=False): def is_valid(self, raise_exception=False):
assert not hasattr(self, 'restore_object'), ( 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. ' 'that is no longer compatible with REST framework 3. '
'Use the new-style `.create()` and `.update()` methods instead.' % 'Use the new-style `.create()` and `.update()` methods instead.' %
self.__class__.__name__ (self.__class__.__module__, self.__class__.__name__)
) )
if not hasattr(self, '_validated_data'): if not hasattr(self, '_validated_data'):
@ -341,9 +341,22 @@ class Serializer(BaseSerializer):
value = self.validate(value) value = self.validate(value)
assert value is not None, '.validate() should return the validated data' assert value is not None, '.validate() should return the validated data'
except ValidationError as exc: except ValidationError as exc:
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({ raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: exc.detail api_settings.NON_FIELD_ERRORS_KEY: exc.detail
}) })
else:
raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
})
return value return value
def to_internal_value(self, data): def to_internal_value(self, data):
@ -507,14 +520,17 @@ class ModelSerializer(Serializer):
self._kwargs['validators'] = validators self._kwargs['validators'] = validators
def create(self, validated_attrs): 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( assert not any(
isinstance(field, BaseSerializer) and not field.read_only isinstance(field, BaseSerializer) and not field.read_only
for field in self.fields.values() for field in self.fields.values()
), ( ), (
'The `.create()` method does not suport nested writable fields ' 'The `.create()` method does not suport nested writable fields '
'by default. Write an explicit `.create()` method for serializer ' 'by default. Write an explicit `.create()` method for serializer '
'%s, or set `read_only=True` on nested serializer fields.' % '`%s.%s`, or set `read_only=True` on nested serializer fields.' %
self.__class__.__name__ (self.__class__.__module__, self.__class__.__name__)
) )
ModelClass = self.Meta.model ModelClass = self.Meta.model
@ -544,8 +560,8 @@ class ModelSerializer(Serializer):
), ( ), (
'The `.update()` method does not suport nested writable fields ' 'The `.update()` method does not suport nested writable fields '
'by default. Write an explicit `.update()` method for serializer ' 'by default. Write an explicit `.update()` method for serializer '
'%s, or set `read_only=True` on nested serializer fields.' % '`%s.%s`, or set `read_only=True` on nested serializer fields.' %
self.__class__.__name__ (self.__class__.__module__, self.__class__.__name__)
) )
for attr, value in validated_attrs.items(): for attr, value in validated_attrs.items():

View File

@ -43,6 +43,32 @@ class TestSerializer:
serializer.data 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: class TestBaseSerializer:
def setup(self): def setup(self):
class ExampleSerializer(serializers.BaseSerializer): class ExampleSerializer(serializers.BaseSerializer):