diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index c10a3f448..50505d30d 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -76,7 +76,28 @@ Deserialization is similar. First we parse a stream into python native datatype When deserializing data, you always need to call `is_valid()` before attempting to access the deserialized object. If any validation errors occur, the `.errors` and `.non_field_errors` properties will contain the resulting error messages. -**TODO: Describe validation in more depth** +### Field-level validation + +You can specify custom field-level validation by adding `validate_()` methods to your `Serializer` subclass. These are analagous to `clean_` methods on Django forms, but accept slightly different arguments. They take a dictionary of deserialized attributes as a first argument, and the field name in that dictionary as a second argument (which will be either the name of the field or the value of the `source` argument to the field, if one was provided). Your `validate_` methods should either just return the attrs dictionary or raise a `ValidationError`. For example: + + from rest_framework import serializers + + class BlogPostSerializer(serializers.Serializer): + title = serializers.CharField(max_length=100) + content = serializers.CharField() + + def validate_title(self, attrs, source): + """ + Check that the blog post is about Django + """ + value = attrs[source] + if "Django" not in value: + raise serializers.ValidationError("Blog post is not about Django") + return attrs + +### Final cross-field validation + +To do any other validation that requires access to multiple fields, add a method called `validate` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`. ## Dealing with nested objects diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 221cbf2fc..c9f025bcb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -208,6 +208,34 @@ class BaseSerializer(Field): return reverted_data + def perform_validation(self, attrs): + """ + Run `validate_()` and `validate()` methods on the serializer + """ + fields = self.get_fields(serialize=False, data=attrs, nested=self.opts.nested) + + for field_name, field in fields.items(): + try: + validate_method = getattr(self, 'validate_%s' % field_name, None) + if validate_method: + source = field.source or field_name + attrs = validate_method(attrs, source) + except ValidationError as err: + self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) + + try: + attrs = self.validate(attrs) + except ValidationError as err: + self._errors['non_field_errors'] = err.messages + + return attrs + + def validate(self, attrs): + """ + Stub method, to be overridden in Serializer subclasses + """ + return attrs + def restore_object(self, attrs, instance=None): """ Deserialize a dictionary of attributes into an object instance. @@ -241,8 +269,9 @@ class BaseSerializer(Field): self._errors = {} if data is not None: attrs = self.restore_fields(data) + attrs = self.perform_validation(attrs) else: - self._errors['non_field_errors'] = 'No input provided' + self._errors['non_field_errors'] = ['No input provided'] if not self._errors: return self.restore_object(attrs, instance=getattr(self, 'object', None)) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index c614b66a0..936f15aa6 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -138,6 +138,55 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.errors, {}) + def test_field_validation(self): + + class CommentSerializerWithFieldValidator(CommentSerializer): + + def validate_content(self, attrs, source): + value = attrs[source] + if "test" not in value: + raise serializers.ValidationError("Test not in value") + return attrs + + data = { + 'email': 'tom@example.com', + 'content': 'A test comment', + 'created': datetime.datetime(2012, 1, 1) + } + + serializer = CommentSerializerWithFieldValidator(data) + self.assertTrue(serializer.is_valid()) + + data['content'] = 'This should not validate' + + serializer = CommentSerializerWithFieldValidator(data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'content': [u'Test not in value']}) + + def test_cross_field_validation(self): + + class CommentSerializerWithCrossFieldValidator(CommentSerializer): + + def validate(self, attrs): + if attrs["email"] not in attrs["content"]: + raise serializers.ValidationError("Email address not in content") + return attrs + + data = { + 'email': 'tom@example.com', + 'content': 'A comment from tom@example.com', + 'created': datetime.datetime(2012, 1, 1) + } + + serializer = CommentSerializerWithCrossFieldValidator(data) + self.assertTrue(serializer.is_valid()) + + data['content'] = 'A comment from foo@bar.com' + + serializer = CommentSerializerWithCrossFieldValidator(data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'non_field_errors': [u'Email address not in content']}) + class MetadataTests(TestCase): def test_empty(self):