Fix ValidationError: {"price": ["A valid number is required."]} in Django
This error means a Django REST Framework serializer rejected the input data during validation. Fix it by ensuring the request data matches the serializer field types, checking that required fields are present, and verifying that custom validate methods return the validated data instead of silently discarding it.
Reading the Stack Trace
Here's what each line means:
- File "/app/api/views.py", line 18, in create: The view calls serializer.is_valid(raise_exception=True) which raises ValidationError if any field fails validation.
- File "/venv/lib/python3.11/site-packages/rest_framework/serializers.py", line 235, in is_valid: DRF's serializer ran all field validators and found that the price field received a non-numeric value.
- rest_framework.exceptions.ValidationError: {"price": ["A valid number is required."]}: The client sent a non-numeric value (like an empty string or text) for a DecimalField.
Common Causes
1. Client sending wrong data type
The API client sends a string or null for a numeric field instead of a valid number.
// Frontend JavaScript
fetch('/api/products/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Widget', price: '' }), // Empty string
});
2. Custom validate method not returning data
A custom validate method runs checks but forgets to return the validated data, causing it to become None.
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['name', 'price', 'category']
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError('Price must be positive.')
# Missing: return value
3. Nested serializer missing many=True
A nested serializer receives a list but is not configured with many=True, causing a type mismatch.
class OrderSerializer(serializers.ModelSerializer):
items = OrderItemSerializer() # Missing many=True for list data
class Meta:
model = Order
fields = ['id', 'items']
The Fix
Always return the validated value from validate_<field> methods. Add null checks for required fields. Use the validate() method for cross-field validation. DRF silently sets the field to None if validate_<field> does not return a value.
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['name', 'price', 'category']
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError('Price must be positive.')
# Missing return!
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['name', 'price', 'category']
def validate_price(self, value):
if value is None:
raise serializers.ValidationError('Price is required.')
if value <= 0:
raise serializers.ValidationError('Price must be positive.')
return value # Always return the validated value
def validate(self, attrs):
"""Cross-field validation."""
if attrs.get('category') == 'free' and attrs.get('price', 0) > 0:
raise serializers.ValidationError('Free products must have a price of 0.')
return attrs
Testing the Fix
import pytest
from rest_framework.test import APITestCase, APIClient
from products.models import Product
class TestProductSerializer(APITestCase):
def setUp(self):
self.client = APIClient()
def test_create_product_with_valid_data(self):
response = self.client.post('/api/products/', {
'name': 'Widget',
'price': '19.99',
'category': 'gadgets',
}, format='json')
assert response.status_code == 201
def test_create_product_with_invalid_price(self):
response = self.client.post('/api/products/', {
'name': 'Widget',
'price': 'not-a-number',
'category': 'gadgets',
}, format='json')
assert response.status_code == 400
assert 'price' in response.json()
def test_create_product_with_negative_price(self):
response = self.client.post('/api/products/', {
'name': 'Widget',
'price': '-5.00',
'category': 'gadgets',
}, format='json')
assert response.status_code == 400
def test_create_product_with_empty_price(self):
response = self.client.post('/api/products/', {
'name': 'Widget',
'price': '',
'category': 'gadgets',
}, format='json')
assert response.status_code == 400
Run your tests:
pytest
Pushing Through CI/CD
git checkout -b fix/serializer-validation-return,git add api/serializers.py,git commit -m "fix: return validated values from serializer validate methods",git push origin fix/serializer-validation-return
Your CI config should look something like this:
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: test_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- run: pip install -r requirements.txt
- run: pytest --tb=short -q
The Full Manual Process: 18 Steps
Here's every step you just went through to fix this one bug:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- Monitor production to confirm the error is resolved
Total time: 30-60 minutes. For one bug.
Or Let bugstack Fix It in Under 2 minutes
Every step above? bugstack does it automatically.
Step 1: Install the SDK
pip install bugstack
Step 2: Initialize
import bugstack
bugstack.init(api_key=os.environ["BUGSTACK_API_KEY"])
Step 3: There is no step 3.
bugstack handles everything from here:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- Deploys to production (or opens a PR for review)
Time from error to fix deployed: Under 2 minutes.
Human involvement: zero.
Try bugstack Free →No credit card. 5-minute setup. Cancel anytime.
Deploying the Fix (Manual Path)
- Run the full test suite locally to confirm serializer validation.
- Open a pull request with the serializer fixes.
- Wait for CI checks to pass on the PR.
- Have a teammate review and approve the PR.
- Merge to main and verify API validation in staging.
Frequently Asked Questions
BugStack runs the fix through your existing test suite, generates tests for valid, invalid, and edge-case inputs for every serializer field, and validates that error responses are properly formatted before marking it safe to deploy.
BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.
Pass error_messages to the serializer field, override default_error_messages on a custom field, or raise serializers.ValidationError with your custom message in validate_<field> methods.
Use raise_exception=True in API views so DRF returns a standardized 400 response. Check manually when you need custom error handling logic before returning the response.