Repository: geopandas/geopandas

Issue:
BUG: empty GeoDataFrame writes invalid JSON (NaN bbox) as parquet metadata
- [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

```python
import geopandas
geopandas.GeoDataFrame(pd.DataFrame({'geometry': None}, index=[])).to_parquet('empty.parquet')
import pyarrow.parquet as pq
print(pq.read_metadata('empty.parquet').metadata[b'geo'])
# JSON metadata has `"bbox": [NaN, NaN, NaN, NaN]` inside
```

#### Problem description

Standard RFC 8259 JSON does not allow NaN numbers, so utilities with strict parsers will fail to parse the metadata completely. As `bbox` is an optional field in the geoparquet specification, it should not have been written in this case at all.

#### Expected Output

There should be no bbox metadata field written in case of fully-empty or missing geometry.

#### Output of ``geopandas.show_versions()``

<details>


SYSTEM INFO
-----------
python     : 3.9.9 (main, Nov 19 2021, 00:00:00)  [GCC 10.3.1 20210422 (Red Hat 10.3.1-1)]
executable : /home/nofitserov/.cache/pypoetry/virtualenvs/test-d9F45URs-py3.9/bin/python
machine    : Linux-5.14.18-100.fc33.x86_64-x86_64-with-glibc2.32

GEOS, GDAL, PROJ INFO
---------------------
GEOS       : 3.8.1
GEOS lib   : /usr/lib64/libgeos_c.so
GDAL       : 3.4.3
GDAL data dir: /home/nofitserov/.cache/pypoetry/virtualenvs/test-d9F45URs-py3.9/lib64/python3.9/site-packages/fiona/gdal_data
PROJ       : 9.1.0
PROJ data dir: /home/nofitserov/.cache/pypoetry/virtualenvs/test-d9F45URs-py3.9/lib64/python3.9/site-packages/pyproj/proj_dir/share/proj

PYTHON DEPENDENCIES
-------------------
geopandas  : 0.12.1
numpy      : 1.23.5
pandas     : 1.5.1
pyproj     : 3.4.0
shapely    : 1.8.5.post1
fiona      : 1.8.22
geoalchemy2: None
geopy      : None
matplotlib : 3.6.2
mapclassify: 2.4.3
pygeos     : None
pyogrio    : v0.4.2
psycopg2   : None
pyarrow    : 10.0.0
rtree      : None


</details>



Discussion:
Thanks for the report! Can confirm the issue. Would you be willing to contribute a fix?

Resolving patch:
diff --git a/geopandas/io/arrow.py b/geopandas/io/arrow.py
--- a/geopandas/io/arrow.py
+++ b/geopandas/io/arrow.py
@@ -2,6 +2,7 @@
 import json
 import warnings
 
+import numpy as np
 from pandas import DataFrame, Series
 
 from geopandas._compat import import_optional_dependency
@@ -108,9 +109,13 @@ def _create_metadata(df, schema_version=None):
             "geometry_type": geometry_types[0]
             if len(geometry_types) == 1
             else geometry_types,
-            "bbox": series.total_bounds.tolist(),
         }
 
+        bbox = series.total_bounds.tolist()
+        if np.isfinite(bbox).all():
+            # don't add bbox with NaNs for empty / all-NA geometry column
+            column_metadata[col]["bbox"] = bbox
+
     return {
         "primary_column": df._geometry_column_name,
         "columns": column_metadata,


Test patch:
diff --git a/geopandas/io/tests/test_arrow.py b/geopandas/io/tests/test_arrow.py
--- a/geopandas/io/tests/test_arrow.py
+++ b/geopandas/io/tests/test_arrow.py
@@ -653,6 +653,20 @@ def test_write_read_feather_expand_user():
     os.remove(os.path.expanduser(test_file))
 
 
+@pytest.mark.parametrize("geometry", [[], [None]])
+def test_write_empty_bbox(tmpdir, geometry):
+    # empty dataframe or all missing geometries -> avoid bbox with NaNs
+    gdf = geopandas.GeoDataFrame({"col": [1] * len(geometry)}, geometry=geometry)
+    gdf.to_parquet(tmpdir / "test.parquet")
+
+    from pyarrow.parquet import read_table
+
+    table = read_table(tmpdir / "test.parquet")
+    metadata = json.loads(table.schema.metadata[b"geo"])
+    assert "encoding" in metadata["columns"]["geometry"]
+    assert "bbox" not in metadata["columns"]["geometry"]
+
+
 @pytest.mark.parametrize("format", ["feather", "parquet"])
 def test_write_read_default_crs(tmpdir, format):
     if format == "feather":