Repository: geopandas/geopandas

Issue:
BUG: OutOfBoundsDatetime on read of faulty datetimes
- [x ] I have checked that this issue has not already been reported.

- [ x] I have confirmed this bug exists on the latest version of geopandas.

- [x ] (optional) I have confirmed this bug exists on the main branch of geopandas.

---

#### Code Sample, a copy-pastable example
Expected behaviour converting 'fully corrupt' datetime to NaT.
```python
import geopandas
from shapely.geometry import Point
import tempfile

# Fully corrupt datetime (row 4)

data = [[1, '1990-02-0300:00:00'],[2, '2002-08-2118:00:00'],[3, '2019-10-3019:00:05'],
        [4, '2008-02-2906:30:45'],[5, '9999-99-9900:00:00']]
geom = [Point(1,1),Point(2,2),Point(3,3),Point(4,4),Point(5,5)]

gdf = geopandas.GeoDataFrame(data = data, columns = ['uid', 'date'], crs = None, geometry=geom)

myschema = {'geometry': 'Point', 'properties': {'uid': 'int', 'date':'datetime'}}

with tempfile.TemporaryDirectory() as td:
    gdf.to_file(f"{td}/temp.gpkg", driver='GPKG', schema=myschema)
    
    print(geopandas.read_file(f"{td}/temp.gpkg"))
```
Unexpected(?) behaviour erroring 'partially corrupt' datetime:

```python
import geopandas
from shapely.geometry import Point
import tempfile

# Semi corrupt datetime (row 4)

data = [[1, '1990-02-0300:00:00'],[2, '2002-08-2118:00:00'],[3, '2019-10-3019:00:05'],
        [4, '2008-02-2906:30:45'],[5, '9999-12-3100:00:00']]
geom = [Point(1,1),Point(2,2),Point(3,3),Point(4,4),Point(5,5)]

gdf = geopandas.GeoDataFrame(data = data, columns = ['uid', 'date'], crs = None, geometry=geom)

myschema = {'geometry': 'Point', 'properties': {'uid': 'int', 'date':'datetime'}}

with tempfile.TemporaryDirectory() as td:
    gdf.to_file(f"{td}/temp.gpkg", driver='GPKG', schema=myschema)
    
    print(geopandas.read_file(f"{td}/temp.gpkg"))
```

This second example returns `OutOfBoundsDatetime: Out of bounds nanosecond timestamp: 9999-12-31 00:00:00`.

#### Problem description

I have a geopackage with datetime in which some datetimes are arbitrarily broken, e.g. strings as '9999-12-3100:00:00', the schema has these set as 'datetime', which means that on read geopandas.io.file is applying `pandas.to_datetime()` to make these strings into datetime objects. This throws an error when these are out of bounds, but curiously only when they're partially out of bounds as shown in the difference between the two examples above.

There would appear to be a solution in which `pandas.to_datetime()` has the `errors` argument defaulting to raise, if instead this is set to coerce or ignore it would allow the file to be read.

I suspect if this were desirable it would have to be as a `geopandas.read_file` argument, the field would then have to be subject to further inspection by the user to resolve any datetime issues, or else it would need to be clearly documented as not guaranteeing datetime objects on read. 

An alternative might be to allow user-defined schemas for read, but that is a more substantial change.

I also accept this might be considered such an edge case that the current behaviour is acceptable.

#### Expected Output

File is read with corrupt datetimes either set to NaT, or ignored and retained as strings. 

#### Output of ``geopandas.show_versions()``

<details>

SYSTEM INFO
-----------
python     : 3.8.5 (default, Mar  2 2021, 09:39:24)  [GCC 9.3.0]
executable : /home/user/.cache/pypoetry/virtualenvs/project-YfbsymT8-py3.8/bin/python
machine    : Linux-5.15.0-1015-aws-x86_64-with-glibc2.29

GEOS, GDAL, PROJ INFO
---------------------
GEOS       : 3.10.2
GEOS lib   : /usr/lib/x86_64-linux-gnu/libgeos_c.so
GDAL       : 3.4.1
GDAL data dir: /home/user/.cache/pypoetry/virtualenvs/project-YfbsymT8-py3.8/lib/python3.8/site-packages/fiona/gdal_data
PROJ       : 8.2.0
PROJ data dir: /home/user/.cache/pypoetry/virtualenvs/project-YfbsymT8-py3.8/lib/python3.8/site-packages/pyproj/proj_dir/share/proj

PYTHON DEPENDENCIES
-------------------
geopandas  : 0.11.0
pandas     : 1.4.1
fiona      : 1.8.21
numpy      : 1.23.1
shapely    : 1.8.1.post1
rtree      : 0.9.7
pyproj     : 3.3.0
matplotlib : 3.5.1
mapclassify: 2.4.3
geopy      : None
psycopg2   : None
geoalchemy2: None
pyarrow    : 4.0.1
pygeos     : 0.12.0

</details>



Discussion:

Resolving patch:
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -341,13 +341,14 @@ def _read_file_fiona(
                     f_filt, crs=crs, columns=columns + ["geometry"]
                 )
             for k in datetime_fields:
-                as_dt = pd.to_datetime(df[k])
+                as_dt = pd.to_datetime(df[k], errors="ignore")
                 # if to_datetime failed, try again for mixed timezone offsets
                 if as_dt.dtype == "object":
-                    # This can fail if there are invalid datetimes
-                    as_dt = pd.to_datetime(df[k], utc=True)
+                    # This can still fail if there are invalid datetimes
+                    as_dt = pd.to_datetime(df[k], errors="ignore", utc=True)
                 # if to_datetime succeeded, round datetimes as
-                # fiona only supports up to ms precision
+                # fiona only supports up to ms precision (any microseconds are
+                # floating point rounding error)
                 if not (as_dt.dtype == "object"):
                     df[k] = as_dt.dt.round(freq="ms")
             return df


Test patch:
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
--- a/geopandas/io/tests/test_file.py
+++ b/geopandas/io/tests/test_file.py
@@ -33,12 +33,14 @@
 try:
     import fiona
 
-    FIONA_GE_1814 = Version(fiona.__version__) >= Version(
-        "1.8.14"
-    )  # datetime roundtrip
+    # datetime roundtrip
+    FIONA_GE_1814 = Version(fiona.__version__) >= Version("1.8.14")
+    # invalid datetime handling
+    FIONA_GE_1821 = Version(fiona.__version__) >= Version("1.8.21")
 except ImportError:
     fiona = False
     FIONA_GE_1814 = False
+    FIONA_GE_1821 = False
 
 
 PYOGRIO_MARK = pytest.mark.skipif(not pyogrio, reason="pyogrio not installed")
@@ -232,7 +234,63 @@ def test_to_file_datetime(tmpdir, driver, ext, time, engine):
         assert_series_equal(df["b"], df_read["b"])
 
 
-def test_read_file_mixed_datetimes(tmpdir):
+dt_exts = ["gpkg", "geojson"]
+
+
+def write_invalid_date_file(date_str, tmpdir, ext, engine):
+    tempfilename = os.path.join(str(tmpdir), f"test_invalid_datetime.{ext}")
+    df = GeoDataFrame(
+        {
+            "date": ["2014-08-26T10:01:23", "2014-08-26T10:01:23", date_str],
+            "geometry": [Point(1, 1), Point(1, 1), Point(1, 1)],
+        }
+    )
+    # Schema not required for GeoJSON since not typed, but needed for GPKG
+    if ext == "geojson":
+        df.to_file(tempfilename)
+    else:
+        schema = {"geometry": "Point", "properties": {"date": "datetime"}}
+        if engine == "pyogrio" and not fiona:
+            # (use schema to write the invalid date without pandas datetimes
+            pytest.skip("test requires fiona kwarg schema")
+        df.to_file(tempfilename, schema=schema, engine="fiona")
+    return tempfilename
+
+
+@pytest.mark.parametrize("ext", dt_exts)
+def test_read_file_datetime_invalid(tmpdir, ext, engine):
+    # https://github.com/geopandas/geopandas/issues/2502
+    if not FIONA_GE_1821 and ext == "gpkg":
+        # https://github.com/Toblerity/Fiona/issues/1035
+        pytest.skip("Invalid datetime throws in Fiona<1.8.21")
+
+    date_str = "9999-99-99T00:00:00"  # invalid date handled by GDAL
+    tempfilename = write_invalid_date_file(date_str, tmpdir, ext, engine)
+    res = read_file(tempfilename)
+    if ext == "gpkg":
+        assert is_datetime64_any_dtype(res["date"])
+        assert pd.isna(res["date"].iloc[-1])
+    else:
+        assert res["date"].dtype == "object"
+        assert isinstance(res["date"].iloc[-1], str)
+
+
+@pytest.mark.parametrize("ext", dt_exts)
+def test_read_file_datetime_out_of_bounds_ns(tmpdir, ext, engine):
+    # https://github.com/geopandas/geopandas/issues/2502
+    if ext == "geojson":
+        skip_pyogrio_not_supported(engine)
+
+    date_str = "9999-12-31T00:00:00"  # valid to GDAL, not to [ns] format
+    tempfilename = write_invalid_date_file(date_str, tmpdir, ext, engine)
+    res = read_file(tempfilename)
+    # Pandas invalid datetimes are read in as object dtype (strings)
+    assert res["date"].dtype == "object"
+    assert isinstance(res["date"].iloc[0], str)
+
+
+def test_read_file_datetime_mixed_offsets(tmpdir):
+    # https://github.com/geopandas/geopandas/issues/2478
     tempfilename = os.path.join(str(tmpdir), "test_mixed_datetime.geojson")
     df = GeoDataFrame(
         {
@@ -244,10 +302,11 @@ def test_read_file_mixed_datetimes(tmpdir):
         }
     )
     df.to_file(tempfilename)
-    res = read_file(tempfilename)  # check mixed tz don't crash GH2478
-    assert is_datetime64_any_dtype(res["date"])
+    # check mixed tz don't crash GH2478
+    res = read_file(tempfilename)
     if FIONA_GE_1814:
         # Convert mixed timezones to UTC equivalent
+        assert is_datetime64_any_dtype(res["date"])
         assert res["date"].dt.tz == pytz.utc
     else:
         # old fiona and pyogrio ignore timezones and read as datetimes successfully