diff --git a/api/api.py b/api/api.py
index d6a8f21843d2e42a41701ecf3340b43924aca372..c14227dbf1ee133e67b02f23a092fbe65225983f 100644
--- a/api/api.py
+++ b/api/api.py
@@ -1,8 +1,10 @@
 import os
 import copy
 import json
+import pytz
 import webapp2
-import bson.json_util
+import datetime
+import bson.objectid
 import webapp2_extras.routes
 
 from . import core
@@ -98,16 +100,25 @@ for cls in [
         ]:
     cls.post_schema = copy.deepcopy(schema_dict[cls.__name__.lower()])
     cls.put_schema = copy.deepcopy(cls.post_schema)
-    cls.put_schema['properties'].pop('_id')
+    cls.put_schema['properties'].pop('_id', None)
     cls.put_schema.pop('required')
 
 
+def custom_json_serializer(obj):
+    if isinstance(obj, bson.objectid.ObjectId):
+        return str(obj)
+    elif isinstance(obj, datetime.datetime):
+        return pytz.timezone('UTC').localize(obj).isoformat()
+    raise TypeError(repr(obj) + " is not JSON serializable")
+
+
 def dispatcher(router, request, response):
     rv = router.default_dispatcher(request, response)
     if rv is not None:
-        response.write(json.dumps(rv, default=bson.json_util.default))
+        response.write(json.dumps(rv, default=custom_json_serializer))
         response.headers['Content-Type'] = 'application/json; charset=utf-8'
 
+
 try:
     import newrelic.agent
     app = newrelic.agent.WSGIApplicationWrapper(webapp2.WSGIApplication(routes))
diff --git a/api/base.py b/api/base.py
index f2bc2c8fb89be77dbf67768ff1ebb157549f2244..51251595b80012c3ac151b50b024099480ffc2f1 100644
--- a/api/base.py
+++ b/api/base.py
@@ -32,11 +32,11 @@ class RequestHandler(webapp2.RequestHandler):
 
         # User (oAuth) authentication
         if access_token and self.app.config['oauth2_id_endpoint']:
-            token_request_time = datetime.datetime.now()
+            token_request_time = datetime.datetime.utcnow()
             cached_token = self.app.db.authtokens.find_one({'_id': access_token})
             if cached_token:
                 self.uid = cached_token['uid']
-                log.debug('looked up cached token in %dms' % ((datetime.datetime.now() - token_request_time).total_seconds() * 1000.))
+                log.debug('looked up cached token in %dms' % ((datetime.datetime.utcnow() - token_request_time).total_seconds() * 1000.))
             else:
                 r = requests.get(self.app.config['oauth2_id_endpoint'], headers={'Authorization': 'Bearer ' + access_token})
                 if r.status_code == 200:
@@ -45,7 +45,7 @@ class RequestHandler(webapp2.RequestHandler):
                     if not self.uid:
                         self.abort(400, 'OAuth2 token resolution did not return email address')
                     self.app.db.authtokens.replace_one({'_id': access_token}, {'uid': self.uid, 'timestamp': datetime.datetime.utcnow()}, upsert=True)
-                    log.debug('looked up remote token in %dms' % ((datetime.datetime.now() - token_request_time).total_seconds() * 1000.))
+                    log.debug('looked up remote token in %dms' % ((datetime.datetime.utcnow() - token_request_time).total_seconds() * 1000.))
                 else:
                     headers = {'WWW-Authenticate': 'Bearer realm="%s", error="invalid_token", error_description="Invalid OAuth2 token."' % self.app.config['site_id']}
                     self.abort(401, 'invalid oauth2 token', headers=headers)
diff --git a/api/collections.py b/api/collections.py
index ab7acb2e98220667019f7d8d87f59ab5ec53d160..9eacbc4dba87278fe9e450ebd1b2d7058b9d2c3d 100644
--- a/api/collections.py
+++ b/api/collections.py
@@ -303,10 +303,7 @@ class CollectionSessions(sessions.Sessions):
         projection['permissions'] = {'$elemMatch': {'_id': self.uid, 'site': self.source_site}}
         sessions = list(self.dbc.find(query, projection)) # avoid permissions checking by not using ContainerList._get()
         for sess in sessions:
-            sess['_id'] = str(sess['_id']) # do this manually, since not going through ContainerList._get()
             sess['subject_code'] = sess.pop('subject', {}).get('code', '') # FIXME when subject is pulled out of session
-            sess.setdefault('timestamp', datetime.datetime.utcnow())
-            sess['timestamp'], sess['timezone'] = util.format_timestamp(sess['timestamp'], sess.get('timezone'))
         if self.debug:
             for sess in sessions:
                 sid = str(sess['_id'])
@@ -342,9 +339,7 @@ class CollectionAcquisitions(acquisitions.Acquisitions):
         projection['permissions'] = {'$elemMatch': {'_id': self.uid, 'site': self.source_site}}
         acquisitions = list(self.dbc.find(query, projection))
         for acq in acquisitions:
-            acq['_id'] = str(acq['_id']) # do this manually, since not going through ContainerList._get()
             acq.setdefault('timestamp', datetime.datetime.utcnow())
-            acq['timestamp'], acq['timezone'] = util.format_timestamp(acq['timestamp'], acq.get('timezone'))
         if self.debug:
             for acq in acquisitions:
                 aid = str(acq['_id'])
diff --git a/api/containers.py b/api/containers.py
index 7d3847d4283ecf1df7bbc6123a2b08cbada418f8..090243dc89c5de3c0914a6e5cbac9be8b9bbd604 100644
--- a/api/containers.py
+++ b/api/containers.py
@@ -137,9 +137,7 @@ class ContainerList(base.RequestHandler):
             projection['permissions'] = {'$elemMatch': {'_id': uid or self.uid, 'site': self.source_site}}
         containers = list(self.dbc.find(query, projection))
         for container in containers:
-            container['_id'] = str(container['_id'])
             container.setdefault('timestamp', datetime.datetime.utcnow())
-            container['timestamp'], container['timezone'] = util.format_timestamp(container['timestamp'], container.get('timezone')) # TODO json serializer should do this
             container['attachment_count'] = len([f for f in container.get('files', []) if f.get('flavor') == 'attachment'])
         return containers
 
@@ -174,11 +172,7 @@ class Container(base.RequestHandler):
         if self.request.GET.get('paths', '').lower() in ('1', 'true'):
             for fileinfo in container['files']:
                 fileinfo['path'] = str(_id)[-3:] + '/' + str(_id) + '/' + fileinfo['filename']
-        container['_id'] = str(container['_id'])
         container.setdefault('timestamp', datetime.datetime.utcnow())
-        container['timestamp'], container['timezone'] = util.format_timestamp(container['timestamp'], container.get('timezone')) # TODO json serializer should do this
-        for note in container.get('notes', []):
-            note['timestamp'], _ = util.format_timestamp(note['timestamp']) # TODO json serializer should do this
         return container, user_perm
 
     def _put(self, _id):
@@ -250,7 +244,7 @@ class Container(base.RequestHandler):
             if self.request.GET.get('info', '').lower() in ('1', 'true'):
                 try:
                     with zipfile.ZipFile(filepath) as zf:
-                        return [(zi.filename, zi.file_size, util.format_timestamp(datetime.datetime(*zi.date_time))[0]) for zi in zf.infolist()]
+                        return [(zi.filename, zi.file_size, datetime.datetime(*zi.date_time)) for zi in zf.infolist()]
                 except zipfile.BadZipfile:
                     self.abort(400, 'not a zip file')
             elif self.request.GET.get('comment', '').lower() in ('1', 'true'):
diff --git a/api/jobs.py b/api/jobs.py
index e88d730476dd357f0133244f8116b89ebf8b1f94..721e10598c33d0381acfc530ea4f975ce900a269 100644
--- a/api/jobs.py
+++ b/api/jobs.py
@@ -164,14 +164,6 @@ def queue_job(db, algorithm_id, container_type, container_id, filename, filehash
     log.info('Running %s as job %s to process %s %s' % (algorithm_id, str(_id), container_type, container_id))
     return _id
 
-def serialize_job(job):
-    if job:
-        job['_id'] = str(job['_id'])
-        job['created'] = util.format_timestamp(job['created'])
-        job['modified'] = util.format_timestamp(job['modified'])
-
-    return job
-
 class Jobs(base.RequestHandler):
 
     """Provide /jobs API routes."""
@@ -184,8 +176,6 @@ class Jobs(base.RequestHandler):
             self.abort(401, 'Request requires superuser')
 
         results = list(self.app.db.jobs.find())
-        for result in results:
-            result = serialize_job(result)
 
         return results
 
@@ -229,7 +219,7 @@ class Jobs(base.RequestHandler):
         if result == None:
             self.abort(400, 'No jobs to process')
 
-        return serialize_job(result)
+        return result
 
 class Job(base.RequestHandler):
 
@@ -240,7 +230,7 @@ class Job(base.RequestHandler):
             self.abort(401, 'Request requires superuser')
 
         result = self.app.db.jobs.find_one({'_id': bson.ObjectId(_id)})
-        return serialize_job(result)
+        return result
 
     def put(self, _id):
         """
diff --git a/api/users.py b/api/users.py
index 06fe0f673978c6139ed3ff04573557f3aba81e33..15477a96f78aec16a852bb1930446b1c19d660ae 100644
--- a/api/users.py
+++ b/api/users.py
@@ -61,9 +61,6 @@ class Users(base.RequestHandler):
         if self.public_request:
             self.abort(403, 'must be logged in to retrieve User list')
         users = list(self.dbc.find({}, {'preferences': False}))
-        for user in users:
-            user['created'], _ = util.format_timestamp(user['created']) # TODO json serializer should do this
-            user['modified'], _ = util.format_timestamp(user['modified']) # TODO json serializer should do this
         if self.debug:
             for user in users:
                 user['debug'] = {}
@@ -108,8 +105,6 @@ class User(base.RequestHandler):
         user = self.dbc.find_one({'_id': _id}, projection or None)
         if not user:
             self.abort(404, 'no such User')
-        user['created'], _ = util.format_timestamp(user['created']) # TODO json serializer should do this
-        user['modified'], _ = util.format_timestamp(user['modified']) # TODO json serializer should do this
         if self.debug and (self.superuser_request or _id == self.uid):
             user['debug'] = {}
             user['debug']['groups'] = self.uri_for('groups', _id, _full=True) + '?' + self.request.query_string
@@ -184,9 +179,6 @@ class Groups(base.RequestHandler):
                     query = {'roles._id': self.uid}
                 projection += ['roles.$']
         groups = list(self.app.db.groups.find(query, projection))
-        for group in groups:
-            group['created'], _ = util.format_timestamp(group['created']) # TODO json serializer should do this
-            group['modified'], _ = util.format_timestamp(group['modified']) # TODO json serializer should do this
         if self.debug:
             for group in groups:
                 group['debug'] = {}
@@ -219,8 +211,6 @@ class Group(base.RequestHandler):
             group = self.app.db.groups.find_one({'_id': _id, 'roles': {'$elemMatch': {'_id': self.uid, 'access': 'admin'}}})
             if not group:
                 self.abort(403, 'User ' + self.uid + ' is not an admin of Group ' + _id)
-        group['created'], _ = util.format_timestamp(group['created']) # TODO json serializer should do this
-        group['modified'], _ = util.format_timestamp(group['modified']) # TODO json serializer should do this
         if self.debug:
             group['debug'] = {}
             group['debug']['projects'] = self.uri_for('g_projects', gid=group['_id'], _full=True) + '?' + self.request.query_string
diff --git a/api/util.py b/api/util.py
index 81683b8140d4b32f746d9ce2854b90b1284889c3..dc9fb2b29003dde7c71f29de5f99f3428b2f78ad 100644
--- a/api/util.py
+++ b/api/util.py
@@ -299,10 +299,5 @@ def guess_filetype(filepath, mimetype):
         return subtype
 
 
-def format_timestamp(timestamp, tzname=None):
-    timezone = pytz.timezone(tzname or 'UTC')
-    return timezone.localize(timestamp).isoformat(), timezone.zone
-
-
 def parse_timestamp(iso_timestamp):
     return dateutil.parser.parse(iso_timestamp)