How to Export GeoJSON in Python with GeoPandas
On this page
Problem statement
Rutland's 21,253 Land Registry parcels arrive in British National Grid. Written straight out with to_file(..., driver="GeoJSON"), they make a 39.8 MB file that GeoPandas reads back perfectly and a web map cannot place, because it takes an easting of 490,000 for a longitude. Written with RFC7946=YES, the same parcels come out in longitude and latitude, without the legacy crs member, in 26.2 MB.
Quick answer
Use GeoDataFrame.to_file() with the GeoJSON driver:
import geopandas as gpd
gdf = gpd.read_file("data/roads.shp")
gdf = gdf.to_crs("EPSG:4326") # recommended for GeoJSON compatibility
gdf.to_file("output/roads.geojson", driver="GeoJSON")
print(len(gdf), "roads written;", gpd.read_file("output/roads.geojson").crs)
7292 roads written; EPSG:4326
Reproject to WGS 84 first: GeoJSON's specification, RFC 7946, defines coordinates as longitude and latitude, and anything that reads the file without GDAL will assume it.
Prepare, inspect, reproject, export, verify
Load or prepare the GeoDataFrame
import geopandas as gpd
gdf = gpd.read_file("data/parcels.shp")
print(type(gdf))
print(gdf.geometry.name)
<class 'geopandas.geodataframe.GeoDataFrame'>
geometry
Inspect the data before export
print(gdf.head(3))
print(gdf.columns.tolist())
print(len(gdf))
print(gdf.geom_type.unique())
print(gdf.crs)
INSPIREID geometry
0 32383481 POLYGON ((495533.11 299678.86, 495533.15 29967...
1 32314855 POLYGON ((494274.49 299788.28, 494274.59 29978...
2 58985263 POLYGON ((494533.53 299672.66, 494532.23 29967...
['INSPIREID', 'geometry']
21253
<ArrowStringArray>
['Polygon']
Length: 1, dtype: str
EPSG:27700
The CRS line is the one that decides the next step.
Reproject to the correct CRS if needed
Convert to EPSG:4326 unless every consumer is known to read a declared CRS:
if gdf.crs != "EPSG:4326":
gdf = gdf.to_crs("EPSG:4326")
print(gdf.crs, gdf.total_bounds.round(4))
EPSG:4326 [-0.835 52.5223 -0.4198 52.7623]
Export the GeoDataFrame to GeoJSON
Use to_file() and set the driver to GeoJSON.
output_file = "output/parcels.geojson"
gdf.to_file(output_file, driver="GeoJSON")
print(open(output_file).read(190))
{
"type": "FeatureCollection",
"name": "parcels",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "IN
Even in WGS 84, GDAL writes the pre-2016 crs member that RFC 7946 dropped; Example 5 asks for RFC 7946 output instead.
Verify the exported GeoJSON file
After export, read the file back in or open it in QGIS.
check_gdf = gpd.read_file("output/parcels.geojson")
print(len(check_gdf))
print(check_gdf.columns.tolist())
print(check_gdf.crs)
21253
['INSPIREID', 'geometry']
EPSG:4326
Rutland parcels and roads written as GeoJSON
The data used on this page
Rutland, England's smallest county: the Land Registry's INSPIRE cadastral parcels (21,253 polygons in British National Grid), OpenStreetMap roads, buildings and land use from the Geofabrik extract, and the 15 electoral wards from the ONS. The roads, parcels and wards are saved as British National Grid shapefiles, the way a UK dataset usually arrives.
import io
import os
import zipfile
import geopandas as gpd
import requests
os.makedirs("data", exist_ok=True)
os.makedirs("output", exist_ok=True)
if not os.path.exists("data/parcels.gpkg"):
reply = requests.Session().get("https://use-land-property-data.service.gov.uk/datasets/"
"inspire/download/Rutland.zip", timeout=300)
reply.raise_for_status()
zipfile.ZipFile(io.BytesIO(reply.content)).extract("Land_Registry_Cadastral_Parcels.gml")
parcels = gpd.read_file("Land_Registry_Cadastral_Parcels.gml")[["INSPIREID", "geometry"]]
parcels.to_file("data/parcels.gpkg")
parcels.to_file("data/parcels.shp")
osm = requests.get("https://download.geofabrik.de/europe/united-kingdom/england/"
"rutland-260101-free.shp.zip", timeout=300).content
with zipfile.ZipFile(io.BytesIO(osm)) as z:
z.extractall("osm")
gpd.read_file("osm/gis_osm_roads_free_1.shp").to_crs(27700).to_file("data/roads.shp")
gpd.read_file("osm/gis_osm_buildings_a_free_1.shp").to_file("data/buildings.gpkg")
gpd.read_file("osm/gis_osm_landuse_a_free_1.shp").to_crs(27700).to_file("data/landuse.shp")
gpd.read_file("https://services1.arcgis.com/ESMARspQHYMw9BZ9/arcgis/rest/services/"
"Wards_December_2024_Boundaries_UK_BGC/FeatureServer/0/query"
"?where=LAD24CD%3D%27E06000017%27&outFields=WD24CD,WD24NM&outSR=4326&f=geojson"
).to_crs(27700).to_file("data/wards.shp")
for name in ("parcels.shp", "roads.shp", "buildings.gpkg", "landuse.shp", "wards.shp"):
layer = gpd.read_file("data/" + name)
print(f"{name:15} {len(layer):>6,} features {layer.crs}")
parcels.shp 21,253 features EPSG:27700
roads.shp 7,292 features EPSG:27700
buildings.gpkg 5,513 features EPSG:4326
landuse.shp 2,405 features EPSG:27700
wards.shp 15 features EPSG:27700
Example 1: Export a GeoDataFrame to GeoJSON
import geopandas as gpd
gdf = gpd.read_file("data/buildings.gpkg")
gdf.to_file("output/buildings.geojson", driver="GeoJSON")
print(len(gdf), "buildings,", gdf.crs)
5513 buildings, EPSG:4326
Example 2: Read a shapefile and convert it to GeoJSON
import geopandas as gpd
gdf = gpd.read_file("data/roads.shp")
gdf.to_file("output/roads_bng.geojson", driver="GeoJSON")
back = gpd.read_file("output/roads_bng.geojson")
print(gdf.crs, "->", back.crs, back.total_bounds.round(0))
EPSG:27700 -> EPSG:27700 [478090. 292420. 508533. 319193.]
This version skips the reprojection. The file is written in British National Grid with a legacy crs member naming EPSG:27700, and GeoPandas reads it back correctly; Leaflet or Mapbox, following RFC 7946, reads 490,000 as a longitude and draws nothing.
Example 3: Reproject to EPSG:4326 before export
import geopandas as gpd
gdf = gpd.read_file("data/wards.shp")
print("Original CRS:", gdf.crs)
gdf_wgs84 = gdf.to_crs("EPSG:4326")
gdf_wgs84.to_file("output/wards_wgs84.geojson", driver="GeoJSON")
print(len(gdf_wgs84), "wards,", gdf_wgs84.total_bounds.round(4))
Original CRS: EPSG:27700
15 wards, [-0.8218 52.5247 -0.4284 52.7598]
Example 4: Export only selected columns and features
import geopandas as gpd
gdf = gpd.read_file("data/landuse.shp")
filtered = gdf[gdf["fclass"] == "residential"][["osm_id", "name", "geometry"]]
filtered = filtered.to_crs("EPSG:4326")
filtered.to_file("output/residential_areas.geojson", driver="GeoJSON")
print(f"{len(filtered)} of {len(gdf)} land-use polygons kept")
159 of 2405 land-use polygons kept
Example 5: File size, precision and RFC 7946
import os
import geopandas as gpd
parcels = gpd.read_file("data/parcels.gpkg")
wgs84 = parcels.to_crs("EPSG:4326")
wgs84.to_file("output/p_default.geojson", driver="GeoJSON")
wgs84.to_file("output/p_6dp.geojson", driver="GeoJSON", COORDINATE_PRECISION=6)
parcels.to_file("output/p_rfc7946.geojson", driver="GeoJSON", RFC7946="YES") # from 27700
wgs84.to_file("output/p.geojsonl", driver="GeoJSONSeq")
for path in ("data/parcels.gpkg", "output/p_default.geojson", "output/p_6dp.geojson",
"output/p_rfc7946.geojson", "output/p.geojsonl"):
print(f"{path:26} {os.path.getsize(path) / 1e6:5.1f} MB")
rfc = gpd.read_file("output/p_rfc7946.geojson")
print("RFC7946 output:", rfc.crs, rfc.total_bounds.round(4),
'"crs" member:', '"crs"' in open("output/p_rfc7946.geojson").read(300))
data/parcels.gpkg 18.5 MB
output/p_default.geojson 39.8 MB
output/p_6dp.geojson 24.5 MB
output/p_rfc7946.geojson 26.2 MB
output/p.geojsonl 26.2 MB
RFC7946 output: EPSG:4326 [-0.835 52.5223 -0.4198 52.7623] "crs" member: False
The Land Registry's 21,253 Rutland parcels take about twice the space as GeoJSON that they take in a GeoPackage: 39.8 MB against 18.5. Six decimal places β about 11 cm β take 38% off, and RFC7946=YES does three things at once: it reprojects to WGS 84 itself, drops the crs member, and writes seven decimal places. The GeoJSONSeq file comes out the same size as the RFC 7946 one, because the sequence driver follows RFC 7946 by default.
Existing files, lists and time zones
File already exists
With pyogrio, to_file() replaces an existing GeoJSON; mode="a" appends to it. Deleting the old file first still makes the intent explicit in the code:
from pathlib import Path
import geopandas as gpd
output_file = Path("output/roads.geojson")
if output_file.exists():
output_file.unlink()
gdf = gpd.read_file("data/roads.shp").to_crs("EPSG:4326")
gdf.head(10).to_file(output_file, driver="GeoJSON")
gdf.head(10).to_file(output_file, driver="GeoJSON") # replaces
gdf.iloc[10:15].to_file(output_file, driver="GeoJSON", mode="a") # appends
print(len(gpd.read_file(output_file)), "features")
15 features
Lists, dates and time zones
Lists and dictionaries in a column are written as JSON arrays and objects, which many consumers cannot handle; flatten them first. Dates and times are written as ISO 8601 text and read back as datetimes. Two things change on the way. A column in Europe/London time comes back with a fixed offset, UTC+01:00, so a winter timestamp written in the same column would come back with a different one; and a column of plain dates comes back as midnight timestamps. Store times in UTC, and convert dates back after reading.
Internal links
- How to Read a Shapefile in Python with GeoPandas
- How to Reproject Spatial Data in Python (GeoPandas)
- How to Perform a Spatial Join in Python (GeoPandas)
- How to reduce coordinate precision safely in Python β precision on write
FAQ
How do I export a GeoDataFrame to GeoJSON in GeoPandas?
Use to_file() with the GeoJSON driver:
gdf.to_file("output/data.geojson", driver="GeoJSON")
That is the standard GeoPandas method to write GeoJSON from a GeoDataFrame.
Can I convert a shapefile to GeoJSON with GeoPandas?
Yes. Read the shapefile, then write it as GeoJSON.
gdf = gpd.read_file("data/input.shp")
gdf.to_file("output/output.geojson", driver="GeoJSON")
Why is my exported GeoJSON not opening correctly in QGIS or a web map?
Common causes include:
- wrong or missing CRS
- invalid geometries
- unsupported attribute values
- corrupted or incomplete output path
Check the CRS, validate geometries, and try reading the exported file back into GeoPandas.
How do I control coordinate precision when exporting GeoJSON?
Pass the GDAL layer option through to_file(), for example gdf.to_file("out.geojson", driver="GeoJSON", COORDINATE_PRECISION=6). Six decimal places of longitude/latitude is roughly 0.1 m and usually keeps file size down without visible loss.
Can I export a GeoJSON that streams line by line for large datasets?
Use the newline-delimited variant by setting the driver to GeoJSONSeq, for example gdf.to_file("out.geojsonl", driver="GeoJSONSeq"). Each feature is written on its own line, which is friendlier for streaming and append workflows than a single large GeoJSON object.
Does exporting to GeoJSON write the CRS into the file?
GDAL writes the old-style crs member, and GeoPandas and QGIS honour it: a file written in EPSG:27700 reads back as EPSG:27700. RFC 7946, the current specification, has no crs member and says the coordinates are WGS 84 longitude and latitude, so web libraries ignore it and misplace the data. Reproject to EPSG:4326 before export, or pass RFC7946="YES" and let GDAL do it.
How do I append features to an existing GeoJSON file with GeoPandas?
Pass mode="a" to to_file(), for example gdf.to_file("out.geojson", driver="GeoJSON", mode="a"), which adds features rather than overwriting β measured with pyogrio 0.13 and GDAL 3.12. The appended data must share the schema and CRS of the existing file, so reproject to EPSG:4326 first.