Repository: pandas-dev/pandas

Issue:
BUG: Series/Dataframe  to_json not returning miliseconds
According to the documentation for pandas 0.12

> date_format : type of date conversion (epoch = epoch milliseconds, iso = ISO8601)
>     default is epoch

The json exporting function seems to be returning the epoch _nanoseconds_ rather than the _milliseconds_ as suggested by the documentation.

``` python

s = pd.Series(1, index=pd.date_range('2013-07-25', periods=2, ))
s.index.astype('int64')
>> array([1374710400000000000, 1374796800000000000])

s.to_json(orient='split', date_format='epoch')
>> '{"name":null,"index":[1374710400000000000,1374796800000000000],"data":[1,1]}'
```



Discussion:
cc @hayd 
cc @Komnomnomnom

I suppose this is incompatible with exportable JSON (e.g. you are writing a frame and using it somewhere else)
This works internally (e.g. writing a frame, then reading it back)

Yeah the expectation would be milliseconds as that seems to be the standard time-stamp format in JSON (and JavaScript). I think it should be JSONifying in millis (or ISO) by default.

Although , afaik the JSON spec says nothing about date and timestamp, so I think this is more of a convention, but in JavaScript it would be milliseconds. 

In JavaScript (Chrome):

``` javascript
> new Date(1374710400000000000)
Invalid Date
> new Date(1374710400000)
Thu Jul 25 2013 10:00:00 GMT+1000 (EST)
> d = new Date(1374710400000)
Thu Jul 25 2013 10:00:00 GMT+1000 (EST)
> d.toJSON()
"2013-07-25T00:00:00.000Z"
> JSON.stringify(d)
""2013-07-25T00:00:00.000Z""
> JSON.stringify(+d)
"1374710400000"
```

fyi...these can actually be fractional msecs (e.g. floats)

I can do this conversion before I pass to you (e.g. I'll just pass you an int or float column), easy enough to reconvert as well (as `pd.to_datetime(values,unit='ms'`) does this type of conversion

or do you want to do it natively?

only other issue is we are going to have to back-compat now (though sort of easy to figure it out) as they differe by orders of magnitude

I'd like to have a go at doing it natively (I think it'd be more flexible that way). Right now (I think) it uses `PyArray_CastToType`, which might make things difficult, but if it doesn't work out we can look at conversion before hitting the JSON parser as you suggested. 

I should have some time to look at it this evening (it's morning here).

fine by me!

let me know


Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -535,7 +535,7 @@ def to_clipboard(self):
         clipboard.to_clipboard(self)
 
     def to_json(self, path_or_buf=None, orient=None, date_format='epoch',
-                double_precision=10, force_ascii=True):
+                double_precision=10, force_ascii=True, date_unit='ms'):
         """
         Convert the object to a JSON string.
 
@@ -566,11 +566,15 @@ def to_json(self, path_or_buf=None, orient=None, date_format='epoch',
               - columns : dict like {column -> {index -> value}}
               - values : just the values array
 
-        date_format : type of date conversion (epoch = epoch milliseconds, iso = ISO8601)
-            default is epoch
+        date_format : string, default 'epoch'
+            type of date conversion, 'epoch' for timestamp, 'iso' for ISO8601
         double_precision : The number of decimal places to use when encoding
             floating point values, default 10.
         force_ascii : force encoded string to be ASCII, default True.
+        date_unit : string, default 'ms' (milliseconds)
+            The time unit to encode to, governs timestamp and ISO8601
+            precision.  One of 's', 'ms', 'us', 'ns' for second, millisecond,
+            microsecond, and nanosecond respectively.
 
         Returns
         -------
@@ -580,8 +584,13 @@ def to_json(self, path_or_buf=None, orient=None, date_format='epoch',
         """
 
         from pandas.io import json
-        return json.to_json(path_or_buf=path_or_buf, obj=self, orient=orient, date_format=date_format,
-                            double_precision=double_precision, force_ascii=force_ascii)
+        return json.to_json(
+            path_or_buf=path_or_buf,
+            obj=self, orient=orient,
+            date_format=date_format,
+            double_precision=double_precision,
+            force_ascii=force_ascii,
+            date_unit=date_unit)
 
 # install the indexerse
 for _name, _indexer in indexing.get_indexers_list():
diff --git a/pandas/io/json.py b/pandas/io/json.py
--- a/pandas/io/json.py
+++ b/pandas/io/json.py
@@ -1,43 +1,51 @@
-
 # pylint: disable-msg=E1101,W0613,W0603
-from pandas.compat import StringIO, long
-from pandas import compat
 import os
 
+import numpy as np
+
+import pandas.json as _json
+from pandas.tslib import iNaT
+from pandas.compat import long
+from pandas import compat, isnull
 from pandas import Series, DataFrame, to_datetime
 from pandas.io.common import get_filepath_or_buffer
-import pandas.json as _json
+
 loads = _json.loads
 dumps = _json.dumps
 
-import numpy as np
-from pandas.tslib import iNaT
-import pandas.lib as lib
 
 ### interface to/from ###
 
-def to_json(path_or_buf, obj, orient=None, date_format='epoch', double_precision=10, force_ascii=True):
+
+def to_json(path_or_buf, obj, orient=None, date_format='epoch',
+            double_precision=10, force_ascii=True, date_unit='ms'):
 
     if isinstance(obj, Series):
-        s = SeriesWriter(obj, orient=orient, date_format=date_format, double_precision=double_precision,
-                         ensure_ascii=force_ascii).write()
+        s = SeriesWriter(
+            obj, orient=orient, date_format=date_format,
+            double_precision=double_precision, ensure_ascii=force_ascii,
+            date_unit=date_unit).write()
     elif isinstance(obj, DataFrame):
-        s = FrameWriter(obj, orient=orient, date_format=date_format, double_precision=double_precision,
-                        ensure_ascii=force_ascii).write()
+        s = FrameWriter(
+            obj, orient=orient, date_format=date_format,
+            double_precision=double_precision, ensure_ascii=force_ascii,
+            date_unit=date_unit).write()
     else:
         raise NotImplementedError
 
     if isinstance(path_or_buf, compat.string_types):
-        with open(path_or_buf,'w') as fh:
+        with open(path_or_buf, 'w') as fh:
             fh.write(s)
     elif path_or_buf is None:
         return s
     else:
         path_or_buf.write(s)
 
+
 class Writer(object):
 
-    def __init__(self, obj, orient, date_format, double_precision, ensure_ascii):
+    def __init__(self, obj, orient, date_format, double_precision,
+                 ensure_ascii, date_unit):
         self.obj = obj
 
         if orient is None:
@@ -47,38 +55,23 @@ def __init__(self, obj, orient, date_format, double_precision, ensure_ascii):
         self.date_format = date_format
         self.double_precision = double_precision
         self.ensure_ascii = ensure_ascii
+        self.date_unit = date_unit
 
         self.is_copy = False
         self._format_axes()
-        self._format_dates()
-
-    def _needs_to_date(self, obj):
-        return obj.dtype == 'datetime64[ns]'
-
-    def _format_dates(self):
-        raise NotImplementedError
 
     def _format_axes(self):
         raise NotImplementedError
 
-    def _format_to_date(self, data):
-
-        # iso
-        if self.date_format == 'iso':
-            return data.apply(lambda x: x.isoformat())
-
-        # int64
-        else:
-            return data.astype(np.int64)
-
-    def copy_if_needed(self):
-        """ copy myself if necessary """
-        if not self.is_copy:
-            self.obj = self.obj.copy()
-            self.is_copy = True
-
     def write(self):
-        return dumps(self.obj, orient=self.orient, double_precision=self.double_precision, ensure_ascii=self.ensure_ascii)
+        return dumps(
+            self.obj,
+            orient=self.orient,
+            double_precision=self.double_precision,
+            ensure_ascii=self.ensure_ascii,
+            date_unit=self.date_unit,
+            iso_dates=self.date_format == 'iso')
+
 
 class SeriesWriter(Writer):
     _default_orient = 'index'
@@ -87,17 +80,7 @@ def _format_axes(self):
         if not self.obj.index.is_unique and self.orient == 'index':
             raise ValueError("Series index must be unique for orient="
                              "'%s'" % self.orient)
-        if self._needs_to_date(self.obj.index):
-            self.copy_if_needed()
-            self.obj.index = self._format_to_date(self.obj.index.to_series())
 
-    def _format_dates(self):
-        if self.obj.dtype == 'datetime64[ns]':
-            self.obj = self._format_to_date(self.obj)
-
-    def _format_bools(self):
-        if self._needs_to_bool(self.obj):
-            self.obj = self._format_to_bool(self.obj)
 
 class FrameWriter(Writer):
     _default_orient = 'columns'
@@ -113,48 +96,19 @@ def _format_axes(self):
             raise ValueError("DataFrame columns must be unique for orient="
                              "'%s'." % self.orient)
 
-        if self.orient == 'columns':
-            axis = 'index'
-        elif self.orient == 'index':
-            axis = 'columns'
-        else:
-            return
-
-        a = getattr(self.obj,axis)
-        if self._needs_to_date(a):
-            self.copy_if_needed()
-            setattr(self.obj,axis,self._format_to_date(a.to_series()))
-
-    def _format_dates(self):
-        dtypes = self.obj.dtypes
-        if len(dtypes[dtypes == 'datetime64[ns]']):
-
-            # need to create a new object
-            d = {}
-
-            for i, (col, c) in enumerate(self.obj.iteritems()):
-
-                if c.dtype == 'datetime64[ns]':
-                    c = self._format_to_date(c)
-
-                d[i] = c
-
-            d = DataFrame(d,index=self.obj.index)
-            d.columns = self.obj.columns
-            self.obj = d
 
 def read_json(path_or_buf=None, orient=None, typ='frame', dtype=True,
               convert_axes=True, convert_dates=True, keep_default_dates=True,
-              numpy=False, precise_float=False):
+              numpy=False, precise_float=False, date_unit=None):
     """
     Convert JSON string to pandas object
 
     Parameters
     ----------
-    filepath_or_buffer : a VALID JSON string or file handle / StringIO. The string could be
-        a URL. Valid URL schemes include http, ftp, s3, and file. For file URLs, a host
-        is expected. For instance, a local file could be
-        file ://localhost/path/to/table.json
+    filepath_or_buffer : a VALID JSON string or file handle / StringIO. The
+        string could be a URL. Valid URL schemes include http, ftp, s3, and
+        file. For file URLs, a host is expected. For instance, a local file
+        could be file ://localhost/path/to/table.json
     orient :
         Series :
           default is 'index'
@@ -169,35 +123,46 @@ def read_json(path_or_buf=None, orient=None, typ='frame', dtype=True,
           and 'records'.
 
         The format of the JSON string
-          split : dict like {index -> [index], columns -> [columns], data -> [values]}
+          split : dict like
+            {index -> [index], columns -> [columns], data -> [values]}
           records : list like [{column -> value}, ... , {column -> value}]
           index : dict like {index -> {column -> value}}
           columns : dict like {column -> {index -> value}}
           values : just the values array
 
     typ : type of object to recover (series or frame), default 'frame'
-    dtype : if True, infer dtypes, if a dict of column to dtype, then use those,
-        if False, then don't infer dtypes at all, default is True,
-        apply only to the data
-    convert_axes : boolean, try to convert the axes to the proper dtypes, default is True
-    convert_dates : a list of columns to parse for dates; If True, then try to parse datelike columns
-        default is True
-    keep_default_dates : boolean, default True. If parsing dates,
-        then parse the default datelike columns
-    numpy : direct decoding to numpy arrays. default is False.Note that the JSON ordering MUST be the same
-        for each term if numpy=True.
-    precise_float : boolean, default False. Set to enable usage of higher precision (strtod) function
-        when decoding string to double values. Default (False) is to use fast but less precise builtin functionality
+    dtype : boolean or dict, default True
+        If True, infer dtypes, if a dict of column to dtype, then use those,
+        if False, then don't infer dtypes at all, applies only to the data.
+    convert_axes : boolean, default True
+        Try to convert the axes to the proper dtypes.
+    convert_dates : boolean, default True
+        List of columns to parse for dates; If True, then try to parse
+        datelike columns default is True
+    keep_default_dates : boolean, default True.
+        If parsing dates, then parse the default datelike columns
+    numpy : boolean, default False
+        Direct decoding to numpy arrays. Note that the JSON ordering MUST be
+        the same for each term if numpy=True.
+    precise_float : boolean, default False.
+        Set to enable usage of higher precision (strtod) function when
+        decoding string to double values. Default (False) is to use fast but
+        less precise builtin functionality
+    date_unit : string, default None
+        The timestamp unit to detect if converting dates. The default behaviour
+        is to try and detect the correct precision, but if this is not desired
+        then pass one of 's', 'ms', 'us' or 'ns' to force parsing only seconds,
+        milliseconds, microseconds or nanoseconds respectively.
 
     Returns
     -------
     result : Series or DataFrame
     """
 
-    filepath_or_buffer,_ = get_filepath_or_buffer(path_or_buf)
+    filepath_or_buffer, _ = get_filepath_or_buffer(path_or_buf)
     if isinstance(filepath_or_buffer, compat.string_types):
         if os.path.exists(filepath_or_buffer):
-            with open(filepath_or_buffer,'r') as fh:
+            with open(filepath_or_buffer, 'r') as fh:
                 json = fh.read()
         else:
             json = filepath_or_buffer
@@ -208,20 +173,32 @@ def read_json(path_or_buf=None, orient=None, typ='frame', dtype=True,
 
     obj = None
     if typ == 'frame':
-        obj = FrameParser(json, orient, dtype, convert_axes, convert_dates, keep_default_dates, numpy).parse()
+        obj = FrameParser(json, orient, dtype, convert_axes, convert_dates,
+                          keep_default_dates, numpy, precise_float,
+                          date_unit).parse()
 
     if typ == 'series' or obj is None:
-        if not isinstance(dtype,bool):
-            dtype = dict(data = dtype)
-        obj = SeriesParser(json, orient, dtype, convert_axes, convert_dates, keep_default_dates, numpy).parse()
+        if not isinstance(dtype, bool):
+            dtype = dict(data=dtype)
+        obj = SeriesParser(json, orient, dtype, convert_axes, convert_dates,
+                           keep_default_dates, numpy, precise_float,
+                           date_unit).parse()
 
     return obj
 
+
 class Parser(object):
 
+    _STAMP_UNITS = ('s', 'ms', 'us', 'ns')
+    _MIN_STAMPS = {
+        's': long(31536000),
+        'ms': long(31536000000),
+        'us': long(31536000000000),
+        'ns': long(31536000000000000)}
+
     def __init__(self, json, orient, dtype=True, convert_axes=True,
                  convert_dates=True, keep_default_dates=False, numpy=False,
-                 precise_float=False):
+                 precise_float=False, date_unit=None):
         self.json = json
 
         if orient is None:
@@ -233,10 +210,20 @@ def __init__(self, json, orient, dtype=True, convert_axes=True,
         if orient == "split":
             numpy = False
 
+        if date_unit is not None:
+            date_unit = date_unit.lower()
+            if date_unit not in self._STAMP_UNITS:
+                raise ValueError('date_unit must be one of %s' %
+                                 (self._STAMP_UNITS,))
+            self.min_stamp = self._MIN_STAMPS[date_unit]
+        else:
+            self.min_stamp = self._MIN_STAMPS['s']
+
         self.numpy = numpy
         self.precise_float = precise_float
-        self.convert_axes  = convert_axes
+        self.convert_axes = convert_axes
         self.convert_dates = convert_dates
+        self.date_unit = date_unit
         self.keep_default_dates = keep_default_dates
         self.obj = None
 
@@ -250,7 +237,8 @@ def parse(self):
         else:
             self._parse_no_numpy()
 
-        if self.obj is None: return None
+        if self.obj is None:
+            return None
         if self.convert_axes:
             self._convert_axes()
         self._try_convert_types()
@@ -259,14 +247,17 @@ def parse(self):
     def _convert_axes(self):
         """ try to convert axes """
         for axis in self.obj._AXIS_NUMBERS.keys():
-            new_axis, result = self._try_convert_data(axis, self.obj._get_axis(axis), use_dtypes=False, convert_dates=True)
+            new_axis, result = self._try_convert_data(
+                axis, self.obj._get_axis(axis), use_dtypes=False,
+                convert_dates=True)
             if result:
-                setattr(self.obj,axis,new_axis)
+                setattr(self.obj, axis, new_axis)
 
     def _try_convert_types(self):
         raise NotImplementedError
 
-    def _try_convert_data(self, name, data, use_dtypes=True, convert_dates=True):
+    def _try_convert_data(self, name, data, use_dtypes=True,
+                          convert_dates=True):
         """ try to parse a ndarray like into a column by inferring dtype """
 
         # don't try to coerce, unless a force conversion
@@ -279,7 +270,8 @@ def _try_convert_data(self, name, data, use_dtypes=True, convert_dates=True):
             else:
 
                 # dtype to force
-                dtype = self.dtype.get(name) if isinstance(self.dtype,dict) else self.dtype
+                dtype = (self.dtype.get(name)
+                         if isinstance(self.dtype, dict) else self.dtype)
                 if dtype is not None:
                     try:
                         dtype = np.dtype(dtype)
@@ -345,7 +337,8 @@ def _try_convert_to_date(self, data):
             was successful """
 
         # no conversion on empty
-        if not len(data): return data, False
+        if not len(data):
+            return data, False
 
         new_data = data
         if new_data.dtype == 'object':
@@ -354,27 +347,29 @@ def _try_convert_to_date(self, data):
             except:
                 pass
 
-
         # ignore numbers that are out of range
-        if issubclass(new_data.dtype.type,np.number):
-            if not ((new_data == iNaT) | (new_data > long(31536000000000000))).all():
+        if issubclass(new_data.dtype.type, np.number):
+            in_range = (isnull(new_data.values) | (new_data > self.min_stamp) |
+                        (new_data.values == iNaT))
+            if not in_range.all():
                 return data, False
 
-        try:
-            new_data = to_datetime(new_data)
-        except:
+        date_units = (self.date_unit,) if self.date_unit else self._STAMP_UNITS
+        for date_unit in date_units:
             try:
-                new_data = to_datetime(new_data.astype('int64'))
+                new_data = to_datetime(new_data, errors='raise',
+                                       unit=date_unit)
+            except OverflowError:
+                continue
             except:
-
-                # return old, noting more we can do
-                return data, False
-
-        return new_data, True
+                break
+            return new_data, True
+        return data, False
 
     def _try_convert_dates(self):
         raise NotImplementedError
 
+
 class SeriesParser(Parser):
     _default_orient = 'index'
 
@@ -410,11 +405,14 @@ def _parse_numpy(self):
                                     precise_float=self.precise_float))
 
     def _try_convert_types(self):
-        if self.obj is None: return
-        obj, result = self._try_convert_data('data', self.obj, convert_dates=self.convert_dates)
+        if self.obj is None:
+            return
+        obj, result = self._try_convert_data(
+            'data', self.obj, convert_dates=self.convert_dates)
         if result:
             self.obj = obj
 
+
 class FrameParser(Parser):
     _default_orient = 'columns'
 
@@ -438,7 +436,8 @@ def _parse_numpy(self):
             self.obj = DataFrame(loads(json, dtype=None, numpy=True,
                                        precise_float=self.precise_float))
         else:
-            self.obj = DataFrame(*loads(json, dtype=None, numpy=True, labelled=True,
+            self.obj = DataFrame(*loads(json, dtype=None, numpy=True,
+                                        labelled=True,
                                         precise_float=self.precise_float))
 
     def _parse_no_numpy(self):
@@ -481,19 +480,22 @@ def _process_converter(self, f, filt=None):
         if needs_new_obj:
 
             # possibly handle dup columns
-            new_obj = DataFrame(new_obj,index=self.obj.index)
+            new_obj = DataFrame(new_obj, index=self.obj.index)
             new_obj.columns = self.obj.columns
             self.obj = new_obj
 
     def _try_convert_types(self):
-        if self.obj is None: return
+        if self.obj is None:
+            return
         if self.convert_dates:
             self._try_convert_dates()
 
-        self._process_converter(lambda col, c: self._try_convert_data(col, c, convert_dates=False))
+        self._process_converter(
+            lambda col, c: self._try_convert_data(col, c, convert_dates=False))
 
     def _try_convert_dates(self):
-        if self.obj is None: return
+        if self.obj is None:
+            return
 
         # our columns to parse
         convert_dates = self.convert_dates
@@ -503,16 +505,18 @@ def _try_convert_dates(self):
 
         def is_ok(col):
             """ return if this col is ok to try for a date parse """
-            if not isinstance(col, compat.string_types): return False
+            if not isinstance(col, compat.string_types):
+                return False
 
             if (col.endswith('_at') or
-                col.endswith('_time') or
-                col.lower() == 'modified' or
-                col.lower() == 'date' or
-                col.lower() == 'datetime'):
-                    return True
+                    col.endswith('_time') or
+                    col.lower() == 'modified' or
+                    col.lower() == 'date' or
+                    col.lower() == 'datetime'):
+                return True
             return False
 
-        self._process_converter(lambda col, c: self._try_convert_to_date(c),
-                                lambda col, c: (self.keep_default_dates and is_ok(col)) or col in convert_dates)
-
+        self._process_converter(
+            lambda col, c: self._try_convert_to_date(c),
+            lambda col, c: ((self.keep_default_dates and is_ok(col))
+                            or col in convert_dates))
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -475,7 +475,8 @@ def pxd(name):
             ext.sources[0] = root + suffix
 
 ujson_ext = Extension('pandas.json',
-                      depends=['pandas/src/ujson/lib/ultrajson.h'],
+                      depends=['pandas/src/ujson/lib/ultrajson.h',
+                               'pandas/src/numpy_helper.h'],
                       sources=['pandas/src/ujson/python/ujson.c',
                                'pandas/src/ujson/python/objToJSON.c',
                                'pandas/src/ujson/python/JSONtoObj.c',
@@ -531,7 +532,8 @@ def pxd(name):
                                   'tests/data/*.xls',
                                   'tests/data/*.xlsx',
                                   'tests/data/*.table',
-                                  'tests/data/*.html'],
+                                  'tests/data/*.html',
+                                  'tests/test_json/data/*.json'],
                     'pandas.tools': ['tests/*.csv'],
                     'pandas.tests': ['data/*.pickle',
                                      'data/*.csv'],


Test patch:
diff --git a/pandas/io/tests/test_json/data/tsframe_iso_v012.json b/pandas/io/tests/test_json/data/tsframe_iso_v012.json
new file mode 100644
--- /dev/null
+++ b/pandas/io/tests/test_json/data/tsframe_iso_v012.json
@@ -0,0 +1 @@
+{"A":{"2000-01-03T00:00:00":1.56808523,"2000-01-04T00:00:00":-0.2550111,"2000-01-05T00:00:00":1.51493992,"2000-01-06T00:00:00":-0.02765498,"2000-01-07T00:00:00":0.05951614},"B":{"2000-01-03T00:00:00":0.65727391,"2000-01-04T00:00:00":-0.08072427,"2000-01-05T00:00:00":0.11805825,"2000-01-06T00:00:00":0.44679743,"2000-01-07T00:00:00":-2.69652057},"C":{"2000-01-03T00:00:00":1.81021139,"2000-01-04T00:00:00":-0.03202878,"2000-01-05T00:00:00":1.629455,"2000-01-06T00:00:00":0.33192641,"2000-01-07T00:00:00":1.28163262},"D":{"2000-01-03T00:00:00":-0.17251653,"2000-01-04T00:00:00":-0.17581665,"2000-01-05T00:00:00":-1.31506612,"2000-01-06T00:00:00":-0.27885413,"2000-01-07T00:00:00":0.34703478},"date":{"2000-01-03T00:00:00":"1992-01-06T18:21:32.120000","2000-01-04T00:00:00":"1992-01-06T18:21:32.120000","2000-01-05T00:00:00":"1992-01-06T18:21:32.120000","2000-01-06T00:00:00":"2013-01-01T00:00:00","2000-01-07T00:00:00":"1992-01-06T18:21:32.120000"}}
\ No newline at end of file
diff --git a/pandas/io/tests/test_json/data/tsframe_v012.json b/pandas/io/tests/test_json/data/tsframe_v012.json
new file mode 100644
--- /dev/null
+++ b/pandas/io/tests/test_json/data/tsframe_v012.json
@@ -0,0 +1 @@
+{"A":{"946857600000000000":1.56808523,"946944000000000000":-0.2550111,"947030400000000000":1.51493992,"947116800000000000":-0.02765498,"947203200000000000":0.05951614},"B":{"946857600000000000":0.65727391,"946944000000000000":-0.08072427,"947030400000000000":0.11805825,"947116800000000000":0.44679743,"947203200000000000":-2.69652057},"C":{"946857600000000000":1.81021139,"946944000000000000":-0.03202878,"947030400000000000":1.629455,"947116800000000000":0.33192641,"947203200000000000":1.28163262},"D":{"946857600000000000":-0.17251653,"946944000000000000":-0.17581665,"947030400000000000":-1.31506612,"947116800000000000":-0.27885413,"947203200000000000":0.34703478},"date":{"946857600000000000":694722092120000000,"946944000000000000":694722092120000000,"947030400000000000":694722092120000000,"947116800000000000":1356998400000000000,"947203200000000000":694722092120000000},"modified":{"946857600000000000":694722092120000000,"946944000000000000":null,"947030400000000000":694722092120000000,"947116800000000000":1356998400000000000,"947203200000000000":694722092120000000}}
\ No newline at end of file
diff --git a/pandas/io/tests/test_json/test_pandas.py b/pandas/io/tests/test_json/test_pandas.py
--- a/pandas/io/tests/test_json/test_pandas.py
+++ b/pandas/io/tests/test_json/test_pandas.py
@@ -1,11 +1,7 @@
-
 # pylint: disable-msg=W0612,E1101
-from copy import deepcopy
-from datetime import datetime, timedelta
-from pandas.compat import range, lrange, StringIO, cPickle as pickle
+from pandas.compat import range, lrange, StringIO
 from pandas import compat
 from pandas.io.common import URLError
-import operator
 import os
 import unittest
 
@@ -37,6 +33,8 @@
 class TestPandasContainer(unittest.TestCase):
 
     def setUp(self):
+        self.dirpath = tm.get_data_path()
+
         self.ts = tm.makeTimeSeries()
         self.ts.name = 'ts'
 
@@ -119,7 +117,8 @@ def _check_orient(df, orient, dtype=None, numpy=False, convert_axes=True, check_
                 check_dtype=False
 
             if not convert_axes and df.index.dtype.type == np.datetime64:
-                unser.index = DatetimeIndex(unser.index.values.astype('i8'))
+                unser.index = DatetimeIndex(
+                    unser.index.values.astype('i8') * 1e6)
             if orient == "records":
                 # index is not captured in this orientation
                 assert_almost_equal(df.values, unser.values)
@@ -280,6 +279,30 @@ def test_frame_to_json_except(self):
         df = DataFrame([1, 2, 3])
         self.assertRaises(ValueError, df.to_json, orient="garbage")
 
+    def test_v12_compat(self):
+        df = DataFrame(
+            [[1.56808523,  0.65727391,  1.81021139, -0.17251653],
+             [-0.2550111, -0.08072427, -0.03202878, -0.17581665],
+             [1.51493992,  0.11805825,  1.629455, -1.31506612],
+             [-0.02765498,  0.44679743,  0.33192641, -0.27885413],
+             [0.05951614, -2.69652057,  1.28163262,  0.34703478]],
+            columns=['A', 'B', 'C', 'D'],
+            index=pd.date_range('2000-01-03', '2000-01-07'))
+        df['date'] = pd.Timestamp('19920106 18:21:32.12')
+        df.ix[3, 'date'] = pd.Timestamp('20130101')
+        df['modified'] = df['date']
+        df.ix[1, 'modified'] = pd.NaT
+
+        v12_json = os.path.join(self.dirpath, 'tsframe_v012.json')
+        df_unser = pd.read_json(v12_json)
+        df_unser = pd.read_json(v12_json)
+        assert_frame_equal(df, df_unser)
+
+        df_iso = df.drop(['modified'], axis=1)
+        v12_iso_json = os.path.join(self.dirpath, 'tsframe_iso_v012.json')
+        df_unser_iso = pd.read_json(v12_iso_json)
+        assert_frame_equal(df_iso, df_unser_iso)
+
     def test_series_non_unique_index(self):
         s = Series(['a', 'b'], index=[1, 1])
 
@@ -295,11 +318,10 @@ def test_series_from_json_to_json(self):
 
         def _check_orient(series, orient, dtype=None, numpy=False):
             series = series.sort_index()
-            unser = read_json(series.to_json(orient=orient), typ='series',
-                              orient=orient, numpy=numpy, dtype=dtype)
+            unser = read_json(series.to_json(orient=orient),
+                              typ='series', orient=orient, numpy=numpy,
+                              dtype=dtype)
             unser = unser.sort_index()
-            #if series.index.dtype.type == np.datetime64:
-            #    unser.index = DatetimeIndex(unser.index.values.astype('i8'))
             if orient == "records" or orient == "values":
                 assert_almost_equal(series.values, unser.values)
             else:
@@ -380,12 +402,12 @@ def test_axis_dates(self):
         # frame
         json = self.tsframe.to_json()
         result = read_json(json)
-        assert_frame_equal(result,self.tsframe)
+        assert_frame_equal(result, self.tsframe)
 
         # series
         json = self.ts.to_json()
-        result = read_json(json,typ='series')
-        assert_series_equal(result,self.ts)
+        result = read_json(json, typ='series')
+        assert_series_equal(result, self.ts)
 
     def test_convert_dates(self):
 
@@ -395,39 +417,84 @@ def test_convert_dates(self):
 
         json = df.to_json()
         result = read_json(json)
-        assert_frame_equal(result,df)
+        assert_frame_equal(result, df)
 
         df['foo'] = 1.
-        json = df.to_json()
-        result = read_json(json,convert_dates=False)
+        json = df.to_json(date_unit='ns')
+        result = read_json(json, convert_dates=False)
         expected = df.copy()
         expected['date'] = expected['date'].values.view('i8')
         expected['foo'] = expected['foo'].astype('int64')
-        assert_frame_equal(result,expected)
+        assert_frame_equal(result, expected)
 
         # series
-        ts = Series(Timestamp('20130101'),index=self.ts.index)
+        ts = Series(Timestamp('20130101'), index=self.ts.index)
         json = ts.to_json()
-        result = read_json(json,typ='series')
-        assert_series_equal(result,ts)
+        result = read_json(json, typ='series')
+        assert_series_equal(result, ts)
+
+    def test_date_format_frame(self):
+        df = self.tsframe.copy()
+
+        def test_w_date(date, date_unit=None):
+            df['date'] = Timestamp(date)
+            df.ix[1, 'date'] = pd.NaT
+            df.ix[5, 'date'] = pd.NaT
+            if date_unit:
+                json = df.to_json(date_format='iso', date_unit=date_unit)
+            else:
+                json = df.to_json(date_format='iso')
+            result = read_json(json)
+            assert_frame_equal(result, df)
+
+        test_w_date('20130101 20:43:42.123')
+        test_w_date('20130101 20:43:42', date_unit='s')
+        test_w_date('20130101 20:43:42.123', date_unit='ms')
+        test_w_date('20130101 20:43:42.123456', date_unit='us')
+        test_w_date('20130101 20:43:42.123456789', date_unit='ns')
+
+        self.assertRaises(ValueError, df.to_json, date_format='iso',
+                          date_unit='foo')
+
+    def test_date_format_series(self):
+        def test_w_date(date, date_unit=None):
+            ts = Series(Timestamp(date), index=self.ts.index)
+            ts.ix[1] = pd.NaT
+            ts.ix[5] = pd.NaT
+            if date_unit:
+                json = ts.to_json(date_format='iso', date_unit=date_unit)
+            else:
+                json = ts.to_json(date_format='iso')
+            result = read_json(json, typ='series')
+            assert_series_equal(result, ts)
+
+        test_w_date('20130101 20:43:42.123')
+        test_w_date('20130101 20:43:42', date_unit='s')
+        test_w_date('20130101 20:43:42.123', date_unit='ms')
+        test_w_date('20130101 20:43:42.123456', date_unit='us')
+        test_w_date('20130101 20:43:42.123456789', date_unit='ns')
 
-    def test_date_format(self):
+        ts = Series(Timestamp('20130101 20:43:42.123'), index=self.ts.index)
+        self.assertRaises(ValueError, ts.to_json, date_format='iso',
+                          date_unit='foo')
 
+    def test_date_unit(self):
         df = self.tsframe.copy()
-        df['date'] = Timestamp('20130101')
-        df_orig = df.copy()
+        df['date'] = Timestamp('20130101 20:43:42')
+        df.ix[1, 'date'] = Timestamp('19710101 20:43:42')
+        df.ix[2, 'date'] = Timestamp('21460101 20:43:42')
+        df.ix[4, 'date'] = pd.NaT
 
-        json = df.to_json(date_format='iso')
-        result = read_json(json)
-        assert_frame_equal(result,df_orig)
+        for unit in ('s', 'ms', 'us', 'ns'):
+            json = df.to_json(date_format='epoch', date_unit=unit)
 
-        # make sure that we did in fact copy
-        assert_frame_equal(df,df_orig)
+            # force date unit
+            result = read_json(json, date_unit=unit)
+            assert_frame_equal(result, df)
 
-        ts = Series(Timestamp('20130101'),index=self.ts.index)
-        json = ts.to_json(date_format='iso')
-        result = read_json(json,typ='series')
-        assert_series_equal(result,ts)
+            # detect date unit
+            result = read_json(json, date_unit=None)
+            assert_frame_equal(result, df)
 
     def test_weird_nested_json(self):
 
diff --git a/pandas/io/tests/test_json/test_ujson.py b/pandas/io/tests/test_json/test_ujson.py
--- a/pandas/io/tests/test_json/test_ujson.py
+++ b/pandas/io/tests/test_json/test_ujson.py
@@ -1,5 +1,4 @@
-﻿import unittest
-from unittest import TestCase
+﻿from unittest import TestCase
 
 try:
     import json
@@ -13,20 +12,17 @@
 import datetime
 import calendar
 import re
-import random
 import decimal
 from functools import partial
 from pandas.compat import range, zip, StringIO, u
-from pandas import compat
 import pandas.json as ujson
 import pandas.compat as compat
 
 import numpy as np
-from pandas.util.testing import assert_almost_equal
 from numpy.testing import (assert_array_equal,
                            assert_array_almost_equal_nulp,
                            assert_approx_equal)
-from pandas import DataFrame, Series, Index
+from pandas import DataFrame, Series, Index, NaT, DatetimeIndex
 import pandas.util.testing as tm
 
 
@@ -327,38 +323,58 @@ def test_encodeFalseConversion(self):
         self.assertEquals(input, json.loads(output))
         self.assertEquals(output, json.dumps(input))
         self.assertEquals(input, ujson.decode(output))
-        pass
 
-    # def test_encodeDatetimeConversion(self):
-    #     ts = time.time()
-    #     input = datetime.datetime.fromtimestamp(ts)
-    #     output = ujson.encode(input)
-    #     expected = calendar.timegm(input.utctimetuple())
-    #     self.assertEquals(int(expected), json.loads(output))
-    #     self.assertEquals(int(expected), ujson.decode(output))
-    #     pass
+    def test_encodeDatetimeConversion(self):
+        ts = time.time()
+        input = datetime.datetime.fromtimestamp(ts)
+        output = ujson.encode(input, date_unit='s')
+        expected = calendar.timegm(input.utctimetuple())
+        self.assertEquals(int(expected), json.loads(output))
+        self.assertEquals(int(expected), ujson.decode(output))
+
+    def test_encodeDateConversion(self):
+        ts = time.time()
+        input = datetime.date.fromtimestamp(ts)
+
+        output = ujson.encode(input, date_unit='s')
+        tup = (input.year, input.month, input.day, 0, 0, 0)
 
-    # def test_encodeDateConversion(self):
-    #     ts = time.time()
-    #     input = datetime.date.fromtimestamp(ts)
+        expected = calendar.timegm(tup)
+        self.assertEquals(int(expected), json.loads(output))
+        self.assertEquals(int(expected), ujson.decode(output))
+
+    def test_nat(self):
+        input = NaT
+        assert ujson.encode(input) == 'null', "Expected null"
 
-    #     output = ujson.encode(input)
-    #     tup = ( input.year, input.month, input.day, 0, 0, 0 )
+    def test_npy_nat(self):
+        from distutils.version import LooseVersion
+        if LooseVersion(np.__version__) < '1.7.0':
+            raise nose.SkipTest
 
-    #     expected = calendar.timegm(tup)
-    #     self.assertEquals(int(expected), json.loads(output))
-    #     self.assertEquals(int(expected), ujson.decode(output))
+        input = np.datetime64('NaT')
+        assert ujson.encode(input) == 'null', "Expected null"
 
-    def test_datetime_nanosecond_unit(self):
-        from datetime import datetime
+    def test_datetime_units(self):
         from pandas.lib import Timestamp
 
-        val = datetime.now()
+        val = datetime.datetime.now()
         stamp = Timestamp(val)
 
-        roundtrip = ujson.decode(ujson.encode(val))
+        roundtrip = ujson.decode(ujson.encode(val, date_unit='s'))
+        self.assert_(roundtrip == stamp.value // 1e9)
+
+        roundtrip = ujson.decode(ujson.encode(val, date_unit='ms'))
+        self.assert_(roundtrip == stamp.value // 1e6)
+
+        roundtrip = ujson.decode(ujson.encode(val, date_unit='us'))
+        self.assert_(roundtrip == stamp.value / 1e3)
+
+        roundtrip = ujson.decode(ujson.encode(val, date_unit='ns'))
         self.assert_(roundtrip == stamp.value)
 
+        self.assertRaises(ValueError, ujson.encode, val, date_unit='foo')
+
     def test_encodeToUTF8(self):
         _skip_if_python_ver(2, 5)
         input = "\xe6\x97\xa5\xd1\x88"
@@ -1267,17 +1283,17 @@ def testIndex(self):
         self.assert_(i.equals(outp))
 
     def test_datetimeindex(self):
-        from pandas.tseries.index import date_range, DatetimeIndex
+        from pandas.tseries.index import date_range
 
         rng = date_range('1/1/2000', periods=20)
 
-        encoded = ujson.encode(rng)
+        encoded = ujson.encode(rng, date_unit='ns')
         decoded = DatetimeIndex(np.array(ujson.decode(encoded)))
 
         self.assert_(rng.equals(decoded))
 
         ts = Series(np.random.randn(len(rng)), index=rng)
-        decoded = Series(ujson.decode(ujson.encode(ts)))
+        decoded = Series(ujson.decode(ujson.encode(ts, date_unit='ns')))
         idx_values = decoded.index.values.astype(np.int64)
         decoded.index = DatetimeIndex(idx_values)
         tm.assert_series_equal(ts, decoded)