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.

Outside GeoJSON there is no single universal order. Longitude is X (east-west) and latitude is Y (north-south), so anything that follows X, Y order writes longitude first. Humans, GPS receivers, Leaflet and the formal EPSG:4326 definition write latitude first. That mismatch is the single most common reason points land in the wrong place.

Coordinate order by format

Format / toolOrderExample (London)
GeoJSON (RFC 7946)lon, lat[-0.1278, 51.5074]
WKT / WKB, PostGIS ST_AsTextlon, lat (X Y)POINT(-0.1278 51.5074)
PostGIS ST_MakePoint(x, y)lon, latST_MakePoint(-0.1278, 51.5074)
Shapefile, GeoPackage geometrylon, lat (X, Y)stored as X, Y
Turf.js, MapLibre GL, OpenLayerslon, lat[-0.1278, 51.5074]
Leaflet L.latLng / L.markerlat, lonL.latLng(51.5074, -0.1278)
Google Maps LatLnglat, lon{lat: 51.5074, lng: -0.1278}
GPS / NMEA, spoken coordinateslat, lon51.5074, -0.1278
EPSG:4326 authority definition, WMS 1.3.0lat, lon51.5074, -0.1278
CSVwhatever the author choseinspect the values

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,lat, 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.

Reading the symptom

Where the wrong point ends up usually tells you what went wrong:

  • Mirrored across the diagonal (Paris at 48.85 N, 2.35 E appearing near 2 N, 48 E in the Indian Ocean): axis-order swap.
  • Sitting exactly at (0, 0) in the Gulf of Guinea ("Null Island"): a missing, null, or zeroed coordinate from a failed parse, not an order problem.
  • Shifted by a consistent small distance everywhere: a datum or transformation issue, not axis order. See why GIS layers do not line up.
  • Clustered near the origin with tiny values: often radians read as degrees, or a decimal-comma parsing error.

The Leaflet trap

Leaflet deliberately matches GPS convention: L.latLng(lat, lng) and L.marker([lat, lng]) take latitude first. The GeoJSON it consumes is longitude first. Copying a coordinate array from a GeoJSON feature into L.marker() flips it. Use L.geoJSON(), which handles the order for you, or L.GeoJSON.coordsToLatLng() when you need a single point. MapLibre GL and OpenLayers use [lng, lat] throughout, which is one small reason teams moving to vector tiles switch from Leaflet to MapLibre.

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.

If the geometry itself is already stored reversed, PostGIS has a dedicated function:

-- Swap X and Y for every vertex, once, after confirming the data is reversed
UPDATE stations SET geom = ST_FlipCoordinates(geom);

-- Verify a known feature now reads lon, lat
SELECT ST_X(geom) AS lon, ST_Y(geom) AS lat FROM stations WHERE name = 'Paris';

Running ST_FlipCoordinates on data that was already correct breaks it the other way, so check a known point before and after.

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.

GDAL 3 and axis order. GDAL 3 honours the authority axis order of a CRS internally, which is why scripts written against GDAL 2 sometimes start swapping after an upgrade. The ogr2ogr and gdalwarp command-line tools default to traditional GIS (lon, lat) order, but code using the OSR API does not unless you call SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER). From GDAL 3.5 you can set it globally with the config option OSR_DEFAULT_AXIS_MAPPING_STRATEGY=TRADITIONAL_GIS_ORDER.

WFS and GeoServer. Request the same layer over WFS 1.0.0 and WFS 2.0.0 and you can get opposite axis orders, because the spec changed which convention governs EPSG:4326. GeoServer exposes per-service settings to override this. When a web map shows points correctly but a script consuming the WFS shows them swapped, the version-dependent axis rule is almost always the cause.

CSV imports. Name the columns explicitly rather than trusting auto-detection:

ogr2ogr -f GPKG points.gpkg points.csv \
  -oo X_POSSIBLE_NAMES=lon,longitude -oo Y_POSSIBLE_NAMES=lat,latitude \
  -a_srs EPSG:4326

In QGIS, Add Delimited Text Layer asks which field is X and which is Y. Picking the latitude column as X is the usual mistake; the preview map shows it immediately.

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] and every first value within [-180, 180].
  • At every boundary between a lat-first library (Leaflet, Google Maps, GPS) and a lon-first one, convert through a single helper function.
  • 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