I built a tiny events API in Django, pointed an Angular form at it, and boom every request died with a ValidationError. The fixes that worked, and a few extras I added so the same bug never trips me again.
My Code
# models.py
from django.db import models
class Event(models.Model):
title = models.CharField(max_length=120)
starts_at = models.DateTimeField()
On the Angular side I sent this:
// Angular (simplified)
$http.post('/api/events/', {
title: 'Launch',
starts_at: new Date('Tue Jun 30 2015 00:00:00 GMT-0500 (CDT)')
});
And the most naïve view ever tried to save it:
# views.py
import json
from django.http import JsonResponse
from .models import Event
def create_event(request):
payload = json.loads(request.body)
Event.objects.create(**payload) # 💥 kaboom
return JsonResponse({'ok': True})
The Crash Report
: ["'Tue Jun 30 2015 00:00:00 GMT-0500 (CDT)' value has an invalid "
"format. It must be in YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."]
At first glance I thought “Django hates time-zones again.”
Not quite the issue was the string itself.
What Django Expects
DateTimeField
only tries a small menu of parsers (all variations of ISO 8601).
Angular’s string contains extras Django does not parse:
- weekday (
Tue
) - month name (
Jun
) - the literal text “GMT” plus offset and zone (
GMT-0500 (CDT)
)
Result: validation fails before my code even runs.
Corect Code
Angular can emit ISO without plugins:
dt = new Date('Tue Jun 30 2015 00:00:00 GMT-0500 (CDT)');
$http.post('/api/events/', {
title: 'Launch',
starts_at: dt.toISOString() // "2015-06-30T05:00:00.000Z"
});
No Django changes, no tears.
If a legacy frontend (or another team) must keep that verbose format, extend Django’s input formats.
With Django REST Framework
# serializers.py
from rest_framework import serializers
from .models import Event
class EventSerializer(serializers.ModelSerializer):
starts_at = serializers.DateTimeField(
input_formats=['%a %b %d %Y %H:%M:%S GMT%z (%Z)']
)
class Meta:
model = Event
fields = '__all__'
Plain Django View
# utils.py
from datetime import datetime
FMT = '%a %b %d %Y %H:%M:%S GMT%z (%Z)'
def parse_browser_datetime(raw):
return datetime.strptime(raw, FMT)
Then:
['starts_at'] = parse_browser_datetime(payload['starts_at'])
Event.objects.create(**payload)
Stretch Goals
Accept both ISO and the verbose string
input_formats = [
'%Y-%m-%dT%H:%M:%S.%fZ', # ISO 8601
'%a %b %d %Y %H:%M:%S GMT%z (%Z)', # browser string
]
starts_at = serializers.DateTimeField(input_formats=input_formats)
Drop-in mixin for any serializer
# mixins.py
class BrowserDateTimeMixin(serializers.Serializer):
browser_formats = [
'%a %b %d %Y %H:%M:%S GMT%z (%Z)',
'%a %b %d %Y %H:%M:%S %Z%z'
]
def build_standard_field(self, name, model_field):
field_class, kwargs = super().build_standard_field(name, model_field)
if isinstance(model_field, models.DateTimeField):
extra = {'input_formats': self.browser_formats + kwargs.get('input_formats', [])}
kwargs.update(extra)
return field_class, kwargs
Unit test so no one breaks it later
rest_framework.test import APITestCase
from django.urls import reverse
class EventAPITests(APITestCase):
def test_verbose_datetime_is_accepted(self):
data = {
"title": "Launch",
"starts_at": "Tue Jun 30 2015 00:00:00 GMT-0500 (CDT)"
}
res = self.client.post(reverse('event-list'), data, format='json')
self.assertEqual(res.status_code, 201)
Angular helper that always returns ISO
// angular-date.service.js
angular.module('app').factory('datetimeISO', () => ({
toISO: d => (d instanceof Date ? d.toISOString() : d)
}));
Inject datetimeISO
wherever you prepare POST bodies.
Final Thought
Whenever I control both ends of an API, I slam everything into ISO 8601 and call it a day it’s the universal handshake computers already understand.
But real projects inherit odd formats, third-party widgets, and historical sins. When that happens, extending input_formats
(or writing one tiny parser) keeps the code honest and keeps clients happy.