Repository: pandas-dev/pandas

Issue:
Iso date timezone is not decoded in Series.read_json
#### Problem description

If I'm trying to load json data thought the Series.read_json considering the following command line :

```python
pd.read_json('{"2019-01-01T11:00:00.000Z":88.352985054,"2019-01-01T12:00:00.000Z":90.091719896,"2019-01-01T13:00:00.000Z":15}', typ='series', orient='index').index
```
I've go the following output 
```
DatetimeIndex(['2019-01-01 11:00:00',
               '2019-01-01 12:00:00', '2019-01-01 13:00:00'],
              dtype='datetime64[ns]', freq=None)
```

The timezone is contained in the 'Z' at the end of the timestamp string. It means that it's a UTC time. More information you can consult the wikipedia page for iso date https://fr.wikipedia.org/wiki/ISO_8601

#### Expected Output
UTC timezone should be mentionned :
```
DatetimeIndex(['2019-01-01 11:00:00',
               '2019-01-01 12:00:00', '2019-01-01 13:00:00'],
              dtype='datetime64[ns, UTC]', freq=None)
```

#### Output of ``pd.show_versions()``

<details>
INSTALLED VERSIONS
------------------
commit: None
python: 3.6.7.final.0
python-bits: 64
OS: Linux
OS-release: 4.15.0-45-generic
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: fr_FR.UTF-8
LOCALE: fr_FR.UTF-8

pandas: 0.24.1
pytest: None
pip: 19.0.3
setuptools: 40.6.2
Cython: None
numpy: 1.16.1
scipy: None
pyarrow: None
xarray: None
IPython: 7.2.0
sphinx: None
patsy: None
dateutil: 2.8.0
pytz: 2018.3
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: None
matplotlib: 3.0.2
openpyxl: 2.6.0
xlrd: 1.2.0
xlwt: None
xlsxwriter: None
lxml.etree: None
bs4: 4.6.3
html5lib: None
sqlalchemy: None
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None
gcsfs: None

[paste the output of ``pd.show_versions()`` here below this line]

</details>



Discussion:
Here we go a workaround directly inspired from the Pandas source code
```
import pandas._libs.json as json
result = json.loads('{"2019-01-01T11:00:00.000Z":88.352985054,"2019-01-01T12:00:00.000Z":90.091719896,"2019-01-01T13:00:00.000Z":15}',
          dtype=None, numpy=True, labelled=True )
pd.Series(result[0], pd.DatetimeIndex(result[1])).index
```

That gave me the following output :
```
DatetimeIndex(['2019-01-01 11:00:00+00:00',
               '2019-01-01 12:00:00+00:00','2019-01-01 13:00:00+00:00'],
              dtype='datetime64[ns, UTC]', freq=None)
```


I _believe_ this vendored cjson parser is doing the work, so I am guessing it is not aware of how to handle `Z`. https://github.com/pandas-dev/pandas/tree/master/pandas/_libs/src/ujson

The fix then potentially would be more involved, but investigations and PRs welcome!
json.loads did not seems to parse date type (with or without timezone)

I think, it should be handled in `pd.Series(..)`

Resolving patch:
diff --git a/pandas/core/tools/datetimes.py b/pandas/core/tools/datetimes.py
--- a/pandas/core/tools/datetimes.py
+++ b/pandas/core/tools/datetimes.py
@@ -200,19 +200,27 @@ def _convert_listlike_datetimes(arg, box, format, name=None, tz=None,
         if format is not None:
             raise ValueError("cannot specify both format and unit")
         arg = getattr(arg, 'values', arg)
-        result = tslib.array_with_unit_to_datetime(arg, unit,
-                                                   errors=errors)
+        result, tz_parsed = tslib.array_with_unit_to_datetime(arg, unit,
+                                                              errors=errors)
         if box:
             if errors == 'ignore':
                 from pandas import Index
                 result = Index(result, name=name)
-                # GH 23758: We may still need to localize the result with tz
-                try:
-                    return result.tz_localize(tz)
-                except AttributeError:
-                    return result
-
-            return DatetimeIndex(result, tz=tz, name=name)
+            else:
+                result = DatetimeIndex(result, name=name)
+            # GH 23758: We may still need to localize the result with tz
+            # GH 25546: Apply tz_parsed first (from arg), then tz (from caller)
+            # result will be naive but in UTC
+            try:
+                result = result.tz_localize('UTC').tz_convert(tz_parsed)
+            except AttributeError:
+                # Regular Index from 'ignore' path
+                return result
+            if tz is not None:
+                if result.tz is None:
+                    result = result.tz_localize(tz)
+                else:
+                    result = result.tz_convert(tz)
         return result
     elif getattr(arg, 'ndim', 1) > 1:
         raise TypeError('arg must be a string, datetime, list, tuple, '


Test patch:
diff --git a/pandas/tests/indexes/datetimes/test_tools.py b/pandas/tests/indexes/datetimes/test_tools.py
--- a/pandas/tests/indexes/datetimes/test_tools.py
+++ b/pandas/tests/indexes/datetimes/test_tools.py
@@ -1967,3 +1967,16 @@ def test_processing_order(self):
         result = pd.to_datetime(300 * 365, unit='D', origin='1870-01-01')
         expected = Timestamp('2169-10-20 00:00:00')
         assert result == expected
+
+    @pytest.mark.parametrize('offset,utc,exp', [
+        ["Z", True, "2019-01-01T00:00:00.000Z"],
+        ["Z", None, "2019-01-01T00:00:00.000Z"],
+        ["-01:00", True, "2019-01-01T01:00:00.000Z"],
+        ["-01:00", None, "2019-01-01T00:00:00.000-01:00"],
+    ])
+    def test_arg_tz_ns_unit(self, offset, utc, exp):
+        # GH 25546
+        arg = "2019-01-01T00:00:00.000" + offset
+        result = to_datetime([arg], unit='ns', utc=utc)
+        expected = to_datetime([exp])
+        tm.assert_index_equal(result, expected)
diff --git a/pandas/tests/io/json/test_pandas.py b/pandas/tests/io/json/test_pandas.py
--- a/pandas/tests/io/json/test_pandas.py
+++ b/pandas/tests/io/json/test_pandas.py
@@ -762,7 +762,10 @@ def test_w_date(date, date_unit=None):
             else:
                 json = df.to_json(date_format='iso')
             result = read_json(json)
-            assert_frame_equal(result, df)
+            expected = df.copy()
+            expected.index = expected.index.tz_localize('UTC')
+            expected['date'] = expected['date'].dt.tz_localize('UTC')
+            assert_frame_equal(result, expected)
 
         test_w_date('20130101 20:43:42.123')
         test_w_date('20130101 20:43:42', date_unit='s')
@@ -784,7 +787,10 @@ def test_w_date(date, date_unit=None):
             else:
                 json = ts.to_json(date_format='iso')
             result = read_json(json, typ='series')
-            assert_series_equal(result, ts)
+            expected = ts.copy()
+            expected.index = expected.index.tz_localize('UTC')
+            expected = expected.dt.tz_localize('UTC')
+            assert_series_equal(result, expected)
 
         test_w_date('20130101 20:43:42.123')
         test_w_date('20130101 20:43:42', date_unit='s')
@@ -880,11 +886,15 @@ def test_round_trip_exception_(self):
 
     @network
     @pytest.mark.single
-    def test_url(self):
+    @pytest.mark.parametrize('field,dtype', [
+        ['created_at', pd.DatetimeTZDtype(tz='UTC')],
+        ['closed_at', 'datetime64[ns]'],
+        ['updated_at', pd.DatetimeTZDtype(tz='UTC')]
+    ])
+    def test_url(self, field, dtype):
         url = 'https://api.github.com/repos/pandas-dev/pandas/issues?per_page=5'  # noqa
         result = read_json(url, convert_dates=True)
-        for c in ['created_at', 'closed_at', 'updated_at']:
-            assert result[c].dtype == 'datetime64[ns]'
+        assert result[field].dtype == dtype
 
     def test_timedelta(self):
         converter = lambda x: pd.to_timedelta(x, unit='ms')
@@ -1298,3 +1308,12 @@ def test_index_false_from_json_to_json(self, orient, index):
         dfjson = expected.to_json(orient=orient, index=index)
         result = read_json(dfjson, orient=orient)
         assert_frame_equal(result, expected)
+
+    def test_read_timezone_information(self):
+        # GH 25546
+        result = read_json('{"2019-01-01T11:00:00.000Z":88}',
+                           typ='series', orient='index')
+        expected = Series([88],
+                          index=DatetimeIndex(['2019-01-01 11:00:00'],
+                                              tz='UTC'))
+        assert_series_equal(result, expected)