Django REST API – Validation and Error Handling
In this post, we will improve our Django REST API by adding validation and better error handling.
Validation helps us control what data is accepted by the API, while error handling makes responses clearer and easier for clients to understand.
Why validation matters
When clients send data to an API, we cannot assume that the data is always correct.
A client may send missing fields, invalid values, duplicated data, or information that does not follow the rules of our application.
Empty project name
Task title too short
Invalid project ID
Completed field with wrong value
Description longer than expected
Validation allows the API to reject invalid data before it is saved to the database.
Client sends data
Serializer validates data
If valid → Save to database
If invalid → Return error response
Where validation happens
In Django REST Framework, most validation happens inside serializers.
Serializers are responsible for converting data, but they are also responsible for checking if incoming data is valid.
Model data → JSON
JSON → Model data
Validate incoming data
Return validation errors
When we call serializer.is_valid(), Django REST Framework checks the submitted data.
if serializer.is_valid():
serializer.save()
else:
return Response(serializer.errors)
With ViewSets and generic views, Django REST Framework handles much of this process automatically.
Current serializers
At this point, our config/projects/serializers.py file should look similar to this:
from rest_framework import serializers
from .models import Project, Task
class ProjectSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Project
fields = '__all__'
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = '__all__'
This works, but we have not added custom validation rules yet.
Field-level validation
Field-level validation is used when we want to validate one specific field.
For example, we may want to prevent project names that are too short.
Update the ProjectSerializer like this:
class ProjectSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Project
fields = '__all__'
def validate_name(self, value):
if len(value) < 3:
raise serializers.ValidationError('Project name must have at least 3 characters.')
return value
The method name must follow this pattern:
validate_fieldname
Because the field is called name, the validation method is called validate_name.
If the value is valid, we return it. If it is invalid, we raise a ValidationError.
Testing field validation
Try creating a project with a name that is too short.
{
"name": "AI",
"description": "A project name that is too short"
}
The API should return a validation error.
{
"name": [
"Project name must have at least 3 characters."
]
}
This tells the client exactly which field is invalid and why.
Adding validation to task titles
We can also add field-level validation to the TaskSerializer.
For example, task titles should not be too short.
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = '__all__'
def validate_title(self, value):
if len(value) < 3:
raise serializers.ValidationError('Task title must have at least 3 characters.')
return value
Now, if a client tries to create a task with a very short title, the API will reject it.
{
"project": 1,
"title": "UI",
"description": "Short title example",
"completed": false
}
Expected error response:
{
"title": [
"Task title must have at least 3 characters."
]
}
Object-level validation
Sometimes validation depends on more than one field.
For example, we may want to prevent a task from being marked as completed if it has no description.
This type of validation is called object-level validation.
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = '__all__'
def validate_title(self, value):
if len(value) < 3:
raise serializers.ValidationError('Task title must have at least 3 characters.')
return value
def validate(self, data):
completed = data.get('completed')
description = data.get('description')
if completed and not description:
raise serializers.ValidationError(
'A completed task must have a description.'
)
return data
The validate method receives all submitted data and can check rules involving multiple fields.
validate_title() → Validates one field
validate() → Validates the full object
Object-level error response
Try creating a completed task without a description.
{
"project": 1,
"title": "Create homepage",
"description": "",
"completed": true
}
The API should return an error response like this:
{
"non_field_errors": [
"A completed task must have a description."
]
}
Because this validation is not attached to one specific field, Django REST Framework returns it under non_field_errors.
Using model field constraints
Some validation can be handled directly by model fields.
For example, our models already define maximum lengths for some fields.
name = models.CharField(max_length=100)
title = models.CharField(max_length=100)
Django REST Framework reads this information from the model and uses it in the serializer.
If a client sends a value that is too long, the serializer can return an error automatically.
{
"title": [
"Ensure this field has no more than 100 characters."
]
}
This means we do not need to manually validate every rule if the model already defines it.
Required fields
Django REST Framework also validates required fields automatically.
For example, the name field in the Project model is required because it does not have blank=True.
name = models.CharField(max_length=100)
If the client sends a project without a name:
{
"description": "Project without a name"
}
The API returns an error:
{
"name": [
"This field is required."
]
}
This automatic validation helps keep the API reliable with less manual code.
Customizing serializer fields
We can also customize fields directly in the serializer.
For example, we can provide custom validation and custom error messages for the name field.
class ProjectSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
name = serializers.CharField(
max_length=100,
error_messages={
'blank': 'Project name cannot be empty.',
'required': 'Project name is required.',
}
)
class Meta:
model = Project
fields = '__all__'
def validate_name(self, value):
if len(value) < 3:
raise serializers.ValidationError('Project name must have at least 3 characters.')
return value
This gives us more control over the error messages returned by the API.
Clear error messages are useful for frontend applications because they can be shown directly to users.
Complete serializers.py file
At this point, your config/projects/serializers.py file could look like this:
from rest_framework import serializers
from .models import Project, Task
class ProjectSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
name = serializers.CharField(
max_length=100,
error_messages={
'blank': 'Project name cannot be empty.',
'required': 'Project name is required.',
}
)
class Meta:
model = Project
fields = '__all__'
def validate_name(self, value):
if len(value) < 3:
raise serializers.ValidationError('Project name must have at least 3 characters.')
return value
class TaskSerializer(serializers.ModelSerializer):
title = serializers.CharField(
max_length=100,
error_messages={
'blank': 'Task title cannot be empty.',
'required': 'Task title is required.',
}
)
class Meta:
model = Task
fields = '__all__'
def validate_title(self, value):
if len(value) < 3:
raise serializers.ValidationError('Task title must have at least 3 characters.')
return value
def validate(self, data):
completed = data.get('completed')
description = data.get('description')
if completed and not description:
raise serializers.ValidationError(
'A completed task must have a description.'
)
return data
This serializer file now includes automatic validation, custom field validation, object-level validation, and custom error messages.
Handling invalid object access
Validation is not only about submitted fields.
We also need to handle cases where a client tries to access an object that does not exist.
GET /api/projects/999/
PATCH /api/tasks/999/
DELETE /api/projects/999/
When using ModelViewSet, Django REST Framework automatically returns a 404 Not Found response if the object does not exist in the queryset.
{
"detail": "Not found."
}
Because our querysets are filtered by the authenticated user, this also protects objects owned by other users.
If the object does not exist → 404 Not Found
If the object belongs to another user → 404 Not Found
This avoids exposing whether another user's object exists in the database.
Handling permission errors
In the previous posts, we used PermissionDenied when a user tries to create a task in another user's project.
from rest_framework.exceptions import PermissionDenied
Example from the TaskViewSet:
def perform_create(self, serializer):
project = serializer.validated_data['project']
if project.owner != self.request.user:
raise PermissionDenied('You cannot create tasks for this project.')
serializer.save()
If the rule fails, the API returns a permission error.
{
"detail": "You cannot create tasks for this project."
}
This gives the client a clear explanation of why the request was rejected.
HTTP status codes for errors
Good APIs use proper HTTP status codes to describe what happened.
400 Bad Request → Invalid data
401 Unauthorized → Authentication required
403 Forbidden → Permission denied
404 Not Found → Resource not found
405 Method Not Allowed → HTTP method not supported
500 Internal Server Error → Unexpected server error
For validation errors, Django REST Framework usually returns 400 Bad Request.
For permission errors, it usually returns 403 Forbidden.
For missing resources, it returns 404 Not Found.
Testing validation in the browsable API
Run the development server:
python manage.py runserver
Then open the tasks endpoint:
Tasks: http://localhost:8000/api/tasks/
Try submitting invalid data through the browsable API form.
{
"project": 1,
"title": "",
"description": "",
"completed": true
}
You should see validation errors returned by the API.
This makes the browsable API a useful tool for testing validation rules while developing.
Testing validation with curl
You can also test validation from the command line using curl.
curl -X POST http://localhost:8000/api/projects/ \ -u your_username:your_password \ -H "Content-Type: application/json" \ -d "{\"name\":\"AI\",\"description\":\"Short project name\"}"
The API should return a validation error because the name has fewer than 3 characters.
{
"name": [
"Project name must have at least 3 characters."
]
}
This confirms that the validation rule is working outside the browser too.
Good error messages
Good error messages should be clear, specific, and useful for the client.
Bad:
Invalid data.
Better:
Project name must have at least 3 characters.
Bad:
Error.
Better:
You cannot create tasks for this project.
Clear errors help frontend developers show better messages to users and make debugging easier.
Recommended approach
For this project, a good validation strategy is:
Use model fields for basic constraints
Use field-level serializer validation for individual fields
Use object-level validation for rules involving multiple fields
Use PermissionDenied for ownership violations
Return clear and specific error messages
Keep validation close to the serializer when possible
This keeps the API predictable and easier to maintain.
What we have learned
In this post, we added validation and improved error handling in our Django REST API.
Serializers validate incoming data
serializer.is_valid() checks if data is valid
Field-level validation validates one specific field
Object-level validation validates multiple fields together
ValidationError returns clear validation responses
Model fields provide automatic validation
PermissionDenied can reject unauthorized actions
DRF returns useful HTTP status codes for errors
Clear error messages improve frontend integration
Our API is now safer, clearer, and more predictable when handling invalid requests.
What comes next?
In the next post, we will start testing our API.
We will learn how to write automated tests for API endpoints, authenticated requests, permissions, and validation rules.
Django REST API: Testing APIs