Geospatial Data Formats: GeoJSON, Shapefile and When Each One Breaks
10 min read · updated August 11, 2026
Most of the trouble these two formats cause comes from three specific decisions in their specifications, and one widespread belief about precision that is exactly backwards.
What RFC 7946 fixed
GeoJSON was standardised by the IETF as RFC 7946 in 2016, replacing a community specification from 2008. Four of its decisions are the ones you meet.
- One coordinate reference system, and it is not optional. The RFC states that all GeoJSON coordinates use WGS 84 “with longitude and latitude units of decimal degrees”. The
crsmember from the 2008 draft “is no longer used”. A file carrying British National Grid eastings in acoordinatesarray is not GeoJSON with a different projection; it is a JSON file that some readers will happily misinterpret as degrees. - Longitude first. Positions are “[longitude and latitude], or easting and northing, precisely in that order”. Every mainstream mapping library that takes
[lat, lng]is therefore inverted relative to the format, and the resulting bug looks like data that is simply somewhere else. - The right-hand rule is advisory in practice. Exterior rings should wind counterclockwise and holes clockwise, but the RFC says parsers “SHOULD NOT reject Polygons that do not follow the right-hand rule” for backwards compatibility. So a wrongly-wound polygon parses everywhere and renders as a hole, or as the entire earth minus your polygon, in the one renderer that takes winding seriously.
- Antimeridian cutting. Any geometry crossing 180° “SHOULD be represented by cutting it in two”. Uncut, a polygon spanning the Pacific is interpreted as spanning the entire globe the other way, which is why maps of Fiji and of Russia break in the same characteristic manner.
What a shapefile actually is
A shapefile is not a file. It is at least three: .shp holding geometry, .shx an index into it, and .dbf a dBASE table of attributes, one record per shape, matched by position. Add .prj for the coordinate system and .cpg for the attribute encoding, both optional and both usually the thing that is missing.
The dBASE inheritance is where the surprises live, and GDAL’s shapefile driver documentation records the consequences precisely. Attribute names “can only be up to 10 characters long”; where truncation creates duplicates, the driver truncates to eight characters and appends a serial number. A column called population_density comes back as population; add population_total and you get populati1 and populati2. Round-trip a dataset through a shapefile and your schema has been rewritten, silently, by a format from 1998.
The other structural limits: one geometry type per file, so points and polygons cannot share a layer; no null geometry; no true curves, so circles arrive as polylines; and no CRS at all unless the .prj travels with it. Coordinates in a shapefile with no .prj are numbers with no units, and the only way to guess is by magnitude — values under 180 are probably degrees, values in the hundreds of thousands are probably a projected grid, and “ probably” is doing the work.
The precision comparison, backwards
Ask which format is more precise and most people say GeoJSON, because it is text and text feels lossless. It is the other way round.
The .shp stores every coordinate as an IEEE 754 double: about 15 to 17 significant decimal digits, which at earth scale is far below a nanometre. It has no precision setting because there is nothing to set. GeoJSON stores whatever digits the writer chose to print, and every writer has a default. RFC 7946 recommends restraint: “6 decimal places (a default common in, e.g., sprintf) amounts to about 10 centimeters”.
Take one polygon vertex near Amsterdam and work the ground distance per decimal place. One degree of latitude is about 111,320 m; one degree of longitude is that times the cosine of the latitude, and at 52°N the cosine is 0.6157.
vertex: 4.891234567, 52.373806789 decimals lat step lon step at 52N 7 0.0111 m 0.0069 m 6 0.111 m 0.069 m <- RFC's "about 10 cm" 5 1.11 m 0.69 m 4 11.1 m 6.9 m 3 111 m 69 m same vertex written at 4 decimals: 4.8912, 52.3738 displacement: up to 5.6 m north-south, 3.4 m east-west
Six decimals is the right default and four is a disaster in a specific way that is worth spelling out. Consider a parcel boundary whose vertices are 3 m apart along a straight edge. At four decimals the longitude grid spacing is 6.9 m, so two consecutive vertices round to the same grid point. The result is a zero-length segment. Feed that into a topology check and you get an invalid geometry; feed it into a simplifier or a buffer and you get a self-intersecting ring; feed it into an area calculation and it may still work, returning an area quietly wrong by the rounding. Nothing in the pipeline announces that the file was written with too few digits.
There is a real cost the other way too. Fifteen digits per ordinate is roughly 17 bytes of JSON text per number against 8 bytes of binary double, so a full-precision GeoJSON of a detailed coastline is several times the size of the equivalent shapefile and, unlike it, must be parsed in its entirety before the first feature is usable. The RFC is blunt about this: implementations “should consider the cost of using a greater precision than necessary”.
The limits that bite
- Shapefile size. GDAL documents that the format “explicitly uses 32bit offsets and so cannot go over 8GB”, that its own implementation is limited to 4 GB, and that it is “not recommended to use a file size over 2GB for both .SHP and .DBF files”. Above that the failures are not clean — you get truncation and readers disagreeing about record counts.
- Attribute encoding. The
.dbfcarries a code page byte that is often unset; GDAL looks for a.cpgsidecar and falls back to the code page in the.dbf. Missing both, non-ASCII place names arrive mangled, and there is no checksum to tell you. - String field widths. GDAL treats an unspecified string field as 80 characters. Longer values are truncated on write.
- GeoJSON has no index. There is nowhere to put one. Any spatial query means parsing the whole document, so a multi-gigabyte GeoJSON is not a large file so much as a file you cannot query at all.
- Neither stores topology. Two administrative units sharing a border store that border twice. Edit one copy and you have a sliver polygon between them, invisible at any normal zoom and fatal to a point-in-polygon test that lands inside it.
What to use instead
Both formats are interchange formats and neither is a working format. For anything you will query repeatedly, three alternatives cover the ground.
GeoPackage is an OGC standard and, underneath, a SQLite database: one file, arbitrary coordinate reference systems, real spatial indexes, no field-name limit, multiple layers of mixed geometry types. It is the direct replacement for a shapefile and there is essentially no argument against it except habit. FlatGeobuf is a binary format with a packed Hilbert-curve R-tree written into the file, which makes it streamable and range-requestable from object storage — the vector analogue of a cloud-optimised GeoTIFF. GeoParquet stores geometry as a column alongside your other columns in Parquet, which is the right shape when the geospatial part is one attribute of a large analytical table rather than the point of it.
Keep GeoJSON for what it is genuinely good at: an API payload, a small hand-editable fixture, something a browser consumes directly. Keep shapefile for the one reason it survives, which is that the other party asked for it. And whatever the container, the coordinate reference system is the part that decides whether a distance means anything — see how projection choice distorts distance.