Where Does Validation Occur?
We can be more confident that our application will not break during runtime if we validate our data is of an expected size/shape/range.
If we have a user account, for example, we don't want to save "larryjohnson" as an email address because when we try to email that address, the application will fail. We might want to restrict names to a reasonable character length, numbers to be only positive, or start dates to be earlier than end dates.
Where Does Validation Occur?
There are many places where data is validated in a frontend + DRF backend situation:
- The user validates what they write when they enter it, assuming they are not malicious
- The browser validates a field or form using HTML5's constraint validation for common use cases, assuming the correct usage of the type attribute on an input field or other validation-related attributes such as required and pattern
- The browser can also validate a field or form in custom situations using JavaScript
- The serializer validates each field on the serializer with their respective validate_<FIELDNAME>(self, value) methods
- The serializer validates the serializer as a whole with the validate(self, attrs) method
- The database validates the data does not violate any database constraints
Simple Validation of Data Types in the Serializer
When using a serializer, DRF maps JSON values back into native Python values. This is the simplest form of validation within our API.
As an example, if the supplied string cannot be coerced into a datetime, DRF will raise a serializers.ValidationError on the field defining the datetime.
Custom Data Validation in the Serializer
TestDriven.io has a good overview of custom data validation in DRF.
As mentioned above, there are two access points for validating your serializer data:
- Field
- Object
When checking serializer validity, first each field is checked, and lastly the object as a whole is checked.
Here's an example serializer with custom validation for the name and rating fields as well as a custom object-level validator:
from rest_framework import serializers
from .models import Product, User
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ["name", "creator_id", "description", "rating"]
def validate_name(self, value):
if value.length > 140:
raise serializers.ValidationError(
"We play by old school Twitter rules here"
)
def validate_rating(self, value):
if value < 1 or value > 5:
raise serializers.ValidationError(
"Rating must be between 1 and 5"
)
return value
def validate(self, data):
user = User.objects.get(id=data["creator_id"])
if Product.objects.filter(
creator=user,
name__iexact=data["name"],
).exists():
raise serializers.ValidationError(
f"A product with this name already exists for creator {user}"
)
Writing Reusable Validators
If you have a validator to reuse in multiple serializers, you can write it separately and then set it on the field under the validators argument.
from rest_framework import serializers
from .models import Product
def is_rating(value):
if value < 1:
raise serializers.ValidationError(
"Rating cannot be lower than 1."
)
if value > 5:
raise serializers.ValidationError(
"Rating cannot be higher than 5."
)
class ProductSerializer(serializers.ModelSerializer):
rating = serializers.IntegerField(validators=[is_rating])
class Meta:
model = Product
fields = ["name", "creator_id", "description", "rating"]