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
Exporting a GeoDataFrame to GeoJSON, with the CRS step that is easy to miss. the one step people skipinspectcrs, dtypesto_crs(4326)required by the specto_file()driver="GeoJSON"verifyread it backGeoJSON is defined in WGS 84 β€” writing projected coordinates produces an invalid file spatialworkflow.io
Everything downstream assumes longitude and latitude β€” reproject before writing.

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

Checks to run before exporting a GeoDataFrame to GeoJSON. before you write the fileCRS is EPSG:4326the GeoJSON specNo null or empty geometriesthey serialise as nullTime zones checkedwritten as fixed offsetsCoordinate precision reducedset_precision()The index is not needed as datareset_index()File size is acceptableGeoJSON is verboseSix decimal places is roughly 11 cm β€” more than that is wasted bytes spatialworkflow.io
The last two decide the size: 18.5 MB of GeoPackage became 39.8 MB of GeoJSON, and 24.5 MB at six decimals.

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
File size of the 21,253 Rutland INSPIRE parcels as a GeoPackage and as GeoJSON with different writer options. the same 21,253 parcels, five waysGeoPackage18.5 MBGeoJSON, default39.8 MBsix decimal places24.5 MBRFC7946=YES26.2 MBGeoJSONSeq26.2 MB020 MB40 MBGDAL 3.12 via pyogrio; the default writes 15 significant digits spatialworkflow.io
Precision is most of the difference; even at six decimals the GeoJSON is a third larger than the GeoPackage.

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

Common GeoJSON export problems and their fixes. what goes wrong on exportSymptomFixCoordinates look like 512345.6to_crs("EPSG:4326") before writingThe file is enormousReduce precision, or useFlatGeobufA date column reads back as atimestampCast it back after readingWeb map shows nothingCheck the CRS and for nullgeometries spatialworkflow.io
The first and last are the same bug seen from two ends of the pipeline.

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.

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.