Django REST Framework serializer 검증
A practical guide to DRF serializer validation — field-level, object-level, custom validators, and handling nested data correctly.
Django REST Framework serializer 검증
Introduction
Django REST Framework (DRF) serializers are the backbone of API data validation. They transform incoming request data into Python objects and validate it before it reaches your views. Yet many developers either skip serializer validation entirely or use only the most basic features, leading to APIs that accept invalid data.
I built a REST API for a trading platform in Lisbon and learned the importance of thorough serializer validation the hard way. An API endpoint accepted a trade order with a negative quantity, which slipped through to the database and caused financial discrepancies.
Environment
Django 4.2.11
Django REST Framework 3.15.1
Python 3.12.3
PostgreSQL 15.4Problem
Problem 1: Basic validation misses edge cases
from rest_framework import serializers
class OrderSerializer(serializers.Serializer):
product_id = serializers.IntegerField()
quantity = serializers.IntegerField()
price = serializers.DecimalField(max_digits=10, decimal_places=2)
# This accepts negative quantities!
serializer = OrderSerializer(data={
"product_id": 1,
"quantity": -5, # Bug: negative quantity
"price": "10.00"
})
serializer.is_valid() # Returns True!Problem 2: Nested serializer validation fails silently
class ItemSerializer(serializers.Serializer):
name = serializers.CharField()
price = serializers.DecimalField(max_digits=10, decimal_places=2)
class OrderSerializer(serializers.Serializer):
items = ItemSerializer(many=True)
total = serializers.DecimalField(max_digits=10, decimal_places=2)
# Nested validation errors are not obvious
serializer = OrderSerializer(data={
"items": [
{"name": "Widget", "price": "10.00"},
{"name": "", "price": "-5.00"} # Invalid: empty name, negative price
],
"total": "5.00"
})
serializer.is_valid()
print(serializer.errors)
# {'items': {1: {'name': ['This field may not be blank.'],
# 'price': ['Ensure this value is greater than or equal to 0.01.']}}}Problem 3: Custom validation not called
class OrderSerializer(serializers.Serializer):
product_id = serializers.IntegerField()
quantity = serializers.IntegerField()
def validate_quantity(self, value):
if value <= 0:
raise serializers.ValidationError("Quantity must be positive")
return value
# This method is never called automatically!
def validate(self, data):
if data['product_id'] == 1 and data['quantity'] > 100:
raise serializers.ValidationError("Too many widgets")
return data
serializer = OrderSerializer(data={
"product_id": 1,
"quantity": -5
})
serializer.is_valid() # Returns True — validate_quantity not called!Analysis
DRF serializers validate data at three levels.
Field-level validation: Each field type has built-in validation. IntegerField checks that the value is an integer, DecimalField checks format and range.
Method-level validation: Custom validate_<field>() methods run after field-level validation. These are for field-specific business rules.
Object-level validation: The validate() method runs after all field-level validations. This is for cross-field validation rules.
Common mistakes:
- Not calling
is_valid()before accessingserializer.data - Using
raise_exception=Trueinvalidate()but not invalidate_<field>() - Forgetting to return the validated data from
validate()methods - Not handling
many=Trueserialization correctly
Check validation errors:
serializer = OrderSerializer(data=invalid_data)
if not serializer.is_valid():
print(serializer.errors)
print(serializer.error_messages)Solution
Fix 1: Add proper field-level validation
from rest_framework import serializers
class OrderSerializer(serializers.Serializer):
product_id = serializers.IntegerField(min_value=1)
quantity = serializers.IntegerField(min_value=1, max_value=10000)
price = serializers.DecimalField(
max_digits=10,
decimal_places=2,
min_value=Decimal("0.01")
)
def validate_quantity(self, value):
if value % 1 != 0:
raise serializers.ValidationError("Quantity must be a whole number")
return valueFix 2: Add cross-field validation
class OrderSerializer(serializers.Serializer):
product_id = serializers.IntegerField()
quantity = serializers.IntegerField(min_value=1)
price = serializers.DecimalField(max_digits=10, decimal_places=2)
discount = serializers.DecimalField(max_digits=5, decimal_places=2, required=False)
def validate(self, data):
"""Object-level validation."""
# Check discount doesn't exceed price
if data.get('discount') and data['discount'] > data['price']:
raise serializers.ValidationError({
'discount': 'Discount cannot exceed price'
})
# Check product-specific rules
if data['product_id'] == 1 and data['quantity'] > 100:
raise serializers.ValidationError({
'quantity': 'Maximum 100 units for product 1'
})
return dataFix 3: Use validators for reusable validation logic
from rest_framework import serializers, validators
def validate_positive_quantity(value):
"""Reusable validator for positive quantity."""
if value <= 0:
raise serializers.ValidationError("Quantity must be positive")
return value
class OrderSerializer(serializers.Serializer):
product_id = serializers.IntegerField()
quantity = serializers.IntegerField(
validators=[validate_positive_quantity]
)
# Or use Validator classes
class OrderQuantityValidator:
def __call__(self, attrs):
if attrs['product_id'] == 1 and attrs['quantity'] > 100:
raise serializers.ValidationError("Too many widgets")
return attrsFix 4: Handle nested serializer validation properly
class ItemSerializer(serializers.Serializer):
name = serializers.CharField(max_length=100)
price = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=Decimal("0.01"))
quantity = serializers.IntegerField(min_value=1)
class OrderSerializer(serializers.Serializer):
items = ItemSerializer(many=True, min_length=1)
notes = serializers.CharField(required=False, allow_blank=True)
def validate_items(self, items):
"""Validate the list of items."""
if len(items) > 50:
raise serializers.ValidationError("Maximum 50 items per order")
return items
# Usage
serializer = OrderSerializer(data={
"items": [
{"name": "Widget", "price": "10.00", "quantity": 5}
],
"notes": ""
})
if serializer.is_valid():
validated_data = serializer.validated_data
# validated_data['items'] is a list of OrderedDictsFix 5: Custom error messages
class OrderSerializer(serializers.Serializer):
product_id = serializers.IntegerField(
error_messages={
'required': 'Product ID is required',
'invalid': 'Product ID must be a valid integer',
'min_value': 'Product ID must be positive',
}
)
class Meta:
# Global error messages
error_messages = {
'invalid': 'Invalid data format',
'null': 'This field cannot be null',
}Lessons Learned
- Always call
is_valid(raise_exception=True)or checkis_valid()before accessingserializer.validated_data. - Use
validate_<field>()for field-specific rules andvalidate()for cross-field rules. - Return the validated data from all validation methods — not just the value.
- Use
validatorsparameter for reusable validation logic across serializers. - Test serializer validation separately from views to catch edge cases early.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.