Short answer

In GeoJSON, every coordinate is written longitude first, latitude second: [longitude, latitude], with an optional third value for elevation, [longitude, latitude, height]. This is fixed by RFC 7946, which also pins the coordinate reference system to WGS84 in decimal degrees. It is deliberately the reverse of how most people say a coordinate out loud ("latitude, longitude"), and that mismatch is the single most common reason GeoJSON points land in the wrong place.

The rule, precisely

RFC 7946 section 3.1.1 states that a position is an array of numbers where "the first two elements are longitude and latitude, or easting and northing, precisely in that order." Two things follow:

  1. Axis order is X, Y, Z — longitude, then latitude, then (optionally) elevation. Always.
  2. The CRS is WGS84. RFC 7946 removed the crs member that existed in the 2008 GeoJSON spec. A modern, conforming file does not and should not carry a crs object; the geographic CRS is assumed to be WGS84 longitude/latitude, identified as urn:ogc:def:crs:OGC::CRS84.

So a point in central London (longitude -0.1278, latitude 51.5074) is:

{ "type": "Point", "coordinates": [-0.1278, 51.5074] }

Write it as [51.5074, -0.1278] and the parser reads longitude 51.5, latitude -0.13: a point in the Indian Ocean off Somalia. The file is still syntactically valid JSON and valid GeoJSON geometry — nothing errors — which is exactly why the bug survives to production.

Why the confusion exists: GeoJSON vs WKT vs EPSG:4326

The axis-order problem is not unique to GeoJSON; the trap is that different standards make different promises.

  • GeoJSON (RFC 7946): always longitude, latitude. No exceptions, no per-file override.
  • WKT geometry (the POINT(x y) text used by PostGIS, GDAL): also X, Y, so POINT(-0.1278 51.5074). This matches GeoJSON.
  • The EPSG definition of 4326: here is the root cause. The authoritative EPSG coordinate system for 4326 declares the axes as latitude, longitude — Y, X. Many OGC services (WMS 1.3.0, some WFS responses) honour that and return lat-first data. Libraries then disagree about whether "EPSG:4326" means lat,lon or lon,lon, and conversions between WKT/GeoJSON and EPSG-aware services are where swaps creep in.

RFC 7946 cut through this by mandating lon,lat for the wire format regardless of what the EPSG axis order says. That is good for interoperability, but it means a developer who trusts "EPSG:4326 is lat,lon" will swap GeoJSON.

How to recognise a swap quickly

A few cheap sanity checks catch almost every axis error:

  • Latitude is bounded by [-90, 90]. If the value you believe is latitude exceeds 90 in absolute terms, the pair is almost certainly swapped. Most of the populated world has |longitude| > |latitude| at low latitudes, so out-of-range second values are a strong tell.
  • Load it on a basemap. Swapped data for the northern hemisphere typically jumps to the equatorial Atlantic, the Gulf of Guinea, or the Indian Ocean — the "wrong ocean" signature.
  • Validate with a parser. geojsonhint, or QGIS / ogrinfo opening the file, will place features where the coordinates actually say, making a swap visually obvious.

Fixing a swapped file

If you have confirmed the values are reversed (lat written first), swap them deterministically. With GDAL's ogr2ogr and an SQL expression you can build correct geometry from the raw numbers, but the cleanest fix depends on how the data is stored. If the file is wrongly tagged but the values are genuinely lon,lat, do nothing to the geometry — just ensure consumers treat it as RFC 7946.

A robust approach when you have the original X/Y attributes is to rebuild geometry explicitly. In PostGIS:

-- raw columns lon and lat are correct numbers; build proper GeoJSON-ready geometry
UPDATE sites
SET geom = ST_SetSRID(ST_MakePoint(lon, lat), 4326);   -- MakePoint is (x=lon, y=lat)

-- export as RFC 7946 GeoJSON
SELECT ST_AsGeoJSON(geom, 6) FROM sites;   -- 6 decimals ~ 0.1 m precision

Note ST_MakePoint(x, y) takes longitude first — the same X,Y order as GeoJSON — which is why building from named lon/lat columns is safer than copying an array you are unsure about.

To convert other formats to conforming GeoJSON, let GDAL handle axis order rather than hand-editing:

ogr2ogr -f GeoJSON out.geojson in.gpkg -lco RFC7946=YES -t_srs EPSG:4326

The RFC7946=YES creation option forces lon,lat output and drops any non-conforming crs member; -t_srs EPSG:4326 ensures the geometry is reprojected to WGS84 first.

Precision, elevation, and the right-hand rule

Two more RFC 7946 details that interact with coordinate order:

  • Precision. Six decimal degrees is roughly 0.11 m at the equator; seven is centimetre-scale. There is rarely a reason to emit 15-digit floats — it bloats files and implies false accuracy. ST_AsGeoJSON(geom, 6) is a sensible default for web delivery.
  • Polygon winding. RFC 7946 specifies that exterior rings should be counter-clockwise and interior rings (holes) clockwise, using the right-hand rule. Many older tools ignored this. Some strict renderers (notably parts of the Mapbox stack) interpret a wrongly-wound exterior ring as covering the whole earth except your polygon. -lco RFC7946=YES fixes winding on export.
  • Antimeridian. Geometries crossing 180 degrees longitude must be split into a MultiGeometry at the antimeridian under RFC 7946; otherwise a line from +179 to -179 renders as a long horizontal streak across the map.

QA / validation checklist

  • Confirm the file has no legacy crs member; if present and not CRS84, treat the file as suspect.
  • Spot-check one or two features against a known location on a basemap.
  • Assert that every second coordinate value (latitude) is within [-90, 90].
  • Run geojsonhint or open in QGIS before shipping.
  • For polygons, verify ring winding (export with RFC7946=YES).
  • Cap coordinate precision at 6-7 decimals for web layers.

Bathyl perspective

Axis-order bugs are cheap to prevent and expensive to discover, because nothing throws an error — the map just quietly lies. We standardise on building geometry from explicitly named longitude/latitude fields, exporting with RFC7946=YES, and validating against a basemap before any handoff. That single discipline removes the most common class of "the points are in the ocean" support tickets.

Related reading

Sources