KunYu
geojsonright-hand-rulepostgisvalidation

GeoJSON Right-Hand Rule Error: What It Means, How to Fix

The GeoJSON right hand rule error means your polygon rings wind the wrong way. Here is what RFC 7946 requires, why PostGIS disagrees, and how to rewind.

KunYu TeamSeptember 11, 202615 min read

What does "Polygons should follow the right-hand rule" actually mean?

The error means at least one polygon ring winds the wrong direction. RFC 7946 requires exterior rings to run counterclockwise and holes to run clockwise. Nothing about your coordinates is wrong, and no vertex has moved. Only the order in which the vertices are listed violates the specification.

The sentence behind the message is short. RFC 7946 Section 3.1.6 says: "A linear ring MUST follow the right-hand rule with respect to the area it bounds, i.e., exterior rings are counterclockwise, and holes are clockwise."

A polygon with one hole, drawn with longitude increasing right and latitude increasing upward. Arrows on the outer boundary run counterclockwise and arrows on the hole run clockwise, so walking the exterior keeps the bounded area on your left.

The literal string most people search for, "Polygons and MultiPolygons should follow the right-hand rule", is not from the RFC. It is the wording used by @mapbox/geojsonhint, and the geojsonlint.com web validator is where developers usually meet it first.

There is a second rule in the same section that gets conflated with winding, and it is worth separating. For a polygon with more than one ring, the first ring MUST be the exterior ring and every other ring MUST be an interior ring. That is about ring position in the array, not ring direction. A file can satisfy one and break the other, and the fixes are different.

Is a wrongly wound polygon actually invalid GeoJSON?

The answer depends on who is asking. The orientation rule is a normative MUST for producers, but the same paragraph tells parsers they SHOULD NOT reject polygons that break it. The validator printing the error is stricter than the specification requires, and it has a documented switch to turn the check off.

The RFC continues immediately after the MUST: "Note: the [GJ2008] specification did not discuss linear ring winding order. For backwards compatibility, parsers SHOULD NOT reject Polygons that do not follow the right-hand rule."

That note exists because winding order was never part of GeoJSON until the format became an IETF RFC in August 2016. The 2008 specification said nothing about it at all, so files and exporters predating RFC 7946 had no reason to wind rings any particular way.

So the practical reading is: fix it if you are writing files that others will consume, but do not assume your data is corrupt. If a linter is the only thing complaining, geojsonhint takes an ignoreRightHandRule option that defaults to false:

const hint = require("@mapbox/geojsonhint");

// Report everything except winding-order violations
const problems = hint.hint(geojsonString, { ignoreRightHandRule: true });
console.log(problems);

One caveat before you build on it: the geojsonhint repository is archived, and its README now points readers to @placemarkio/check-geojson instead. The option still works in the published package; it is the maintenance that stopped.

Turning the check off is the right call when you control both ends of the pipe and something downstream renders correctly anyway. It is the wrong call when you are publishing. If you want to see which specific features trip the rule before deciding, run the file through a GeoJSON validator first.

We had to pick a side on this when we built our own validator. It reports a clockwise exterior or a counterclockwise hole as a warning, never an error, for the reason the RFC itself gives: a file that breaks the winding rule almost always still works, and calling it an error sends people off to fix something that is not broken. What it does report as errors are the failures that make a ring meaningless on its own terms, such as a ring whose first and last positions differ, or one with fewer than four positions. It does not test for self-intersection at all — worth knowing before you read a clean result as a clean polygon.

Which tools throw this error, and which just mis-render silently

Almost nobody agrees on the response. Some tools print the literal error string, some refuse the file with a generic message that names nothing useful, and some render it wrong without complaining at all. Matching your symptom to the table below tells you how urgent the fix really is.

Tool What happens Evidence
geojsonlint.com / @mapbox/geojsonhint Prints the exact error text Default behavior, switchable via ignoreRightHandRule; repo archived
Mapbox GL Draw Throws "Polygons and MultiPolygons should follow the right-hand rule" on Draw.add() for a counterclockwise-drawn polygon Reported present in v0.11.18+, absent in v0.11.15 (issue #514)
Leaflet In one reported case (a QGIS 3.30.1 export), refused the file with only "Invalid GeoJSON" QGIS issue #52607
Leaflet (MultiPolygon holes) Renders clockwise holes incorrectly; plain Polygon with holes did not regress the same way Leaflet issue #5384, open
OpenLayers Writes rings exactly as constructed unless you ask otherwise rightHanded defaults to undefined in the GeoJSON format API
QGIS 3.30.1 GeoJSON export Produced a file that geojsonlint.com flagged Issue #52607, closed as "not planned"
MongoDB 2dsphere Documentation never mentions winding 2dsphere index docs define validity by ring containment and non-self-intersection

Two entries deserve caveats. The Leaflet thread documents the regression but has no maintainer resolution, so "Leaflet rejects wrong winding" is too strong a summary; its tolerance varies by version and geometry type. And on OpenLayers, one user reported that setting rightHanded: true on writeFeatureObject still produced output that a downstream consumer rejected for right-hand-rule reasons, with no answer in the thread.

MongoDB is the honest gap here. Its documentation is silent on winding, and we could not find a primary source stating whether the 2dsphere index rejects, silently accepts, or auto-corrects a wrongly wound polygon at insert time. Test it against your own server rather than trusting a blog claim, including this one.

Why "right-hand rule" means the opposite in PostGIS and d3-geo

Because two different conventions share one name. GeoJSON's right-hand rule puts the interior on your left as you walk the exterior ring counterclockwise. PostGIS's ST_ForceRHR puts the bounded area on the right, which winds exteriors clockwise. Same phrase, mirrored result, and PostGIS documents the clash itself.

Two panels showing the same square exterior ring. Under GeoJSON RFC 7946 the arrows run counterclockwise with holes clockwise; under PostGIS ST_ForceRHR they run clockwise with holes counterclockwise, because it places the bounded area to the right of the boundary.

Convention Exterior ring Interior rings
GeoJSON RFC 7946 Counterclockwise Clockwise
PostGIS ST_ForceRHR / ST_ForcePolygonCW Clockwise Counterclockwise
PostGIS ST_ForcePolygonCCW Counterclockwise Clockwise
d3-geo, polygon smaller than a hemisphere Clockwise Not stated in the cited source
d3-geo, polygon larger than a hemisphere Anticlockwise Not stated in the cited source

The PostGIS documentation for ST_ForceRHR describes it as orienting vertices so that "the area that is bounded by the polygon is to the right of the boundary", and then warns in its own text that "the above definition of the Right-Hand-Rule conflicts with definitions used in other contexts", recommending ST_ForcePolygonCW instead.

d3-geo's model is spherical rather than planar, so the sign of a ring decides which side of the sphere is inside. The Observable explainer on winding order in D3 puts the symptom list plainly: "If polygons disappear, holes fill in, or the entire world appears filled except your intended polygon, winding order is a likely cause." None of it produces an error message.

The dispute goes deeper than tooling. Errata ID 5069 against RFC 7946 argues the RFC's own wording is backwards and should read "exterior rings are clockwise, and holes are counterclockwise", on the grounds that the stated result contradicts the right-hand-rule principle the text invokes. Its status is "Reported", not verified or accepted, so implementations follow the published text. Treat it as a sign that the phrase is genuinely ambiguous, not as license to rewind the other way.

How do I tell which way a ring is wound?

Compute the signed area with the shoelace formula and look at the sign. Positive means counterclockwise, negative means clockwise, in the standard orientation where longitude is x and latitude is y. That is the same test Turf's rewind uses, and it takes about ten lines of code.

function signedArea(ring) {
  let sum = 0;
  for (let i = 0, last = ring.length - 1; i < last; i++) {
    const [x1, y1] = ring[i];
    const [x2, y2] = ring[i + 1];
    sum += x1 * y2 - x2 * y1;
  }
  return sum / 2;
}

signedArea([[0, 0], [1, 1], [1, 0], [0, 0]]); // -0.5  clockwise
signedArea([[0, 0], [1, 0], [1, 1], [0, 0]]); //  0.5  counterclockwise

Those two rings are the documented examples for @turf/boolean-clockwise, which returns true for the first and false for the second. The signs above match, which is a useful sanity check that your loop is correct before you point it at real data.

One trap: the sign is meaningless if your coordinate pairs are [lat, lon] instead of [lon, lat]. Swapping the axes negates the signed area and turns every ring into its mirror image, so a "winding" bug can really be an axis-order bug. The same swap is what sends points to coordinates in the ocean, and it is worth ruling out before you rewind anything.

Fixing winding order in JavaScript and in the browser

In most cases this is one function call. turf.rewind defaults to counterclockwise exteriors and clockwise holes, which is exactly what RFC 7946 asks for, so you rarely need to pass options. If you would rather not install anything, a browser tool does the same rewind on pasted GeoJSON.

const turf = require("@turf/turf");
const fs = require("fs");

const fc = JSON.parse(fs.readFileSync("in.geojson", "utf8"));

// options.reverse defaults to false:
// outer ring counterclockwise, inner rings clockwise (shoelace formula)
const fixed = turf.rewind(fc);

fs.writeFileSync("out.geojson", JSON.stringify(fixed));

The rewind documentation confirms the default: with options.reverse left at false, outer rings come out counterclockwise and inner rings clockwise. Passing { reverse: true } gives you the PostGIS/d3 orientation instead, which is occasionally what a downstream consumer actually wants.

Check the version before you trust it blindly. An issue titled Exterior ring CCW for @turf/rewind reported that an earlier release of the module did not conform to the exterior-CCW convention, with the fix scheduled for patch release v4.3.1. Check what version your lockfile pins.

For one-off files there is a zero-install path. The Mapster Right-Hand Rule Fixer, built on the Mapbox geojson-rewind Node library, takes pasted GeoJSON and rewinds it on a button press. If you would rather see the geometry while you change it, a GeoJSON editor is the better place to work.

Fixing winding order in Python

Python gives you three short options, and which one fits depends on where your geometries already live. geojson-rewind works on whole GeoJSON documents and ships a command-line entry point. Shapely's orient() handles one polygon at a time. Shapely 2.1 added orient_polygons, which applies the same rule across multipolygons and collections in one vectorized call.

The geojson-rewind package exposes a single rewind(input) function that accepts a GeoJSON string or a dict and returns the same type, enforcing counterclockwise exteriors and clockwise holes. It requires Python 3.10 or newer.

import json
from geojson_rewind import rewind

with open("in.geojson") as f:
    gj = json.load(f)

fixed = rewind(gj)  # dict in, dict out

with open("out.geojson", "w") as f:
    json.dump(fixed, f)

Same package, no Python at all:

rewind in.geojson > out.geojson

If your geometries already live in Shapely objects, stay there. The legacy helper takes a sign argument, and sign=1.0 is the RFC 7946 orientation:

from shapely.geometry import shape, mapping
from shapely.geometry.polygon import orient

geom = shape(feature["geometry"])
feature["geometry"] = mapping(orient(geom, sign=1.0))  # exterior CCW, holes CW

On Shapely 2.1.0 and later, shapely.orient_polygons replaces the per-geometry loop and handles GeometryCollections too:

import shapely

# exterior_cw=False (the default) -> CCW exteriors, CW interiors
fixed = shapely.orient_polygons(geoms, exterior_cw=False)

Setting exterior_cw=True reverses it, which is what you want when feeding a system that follows the PostGIS convention.

Fixing it at export time with ogr2ogr and PostGIS

It is cheaper to stop emitting the wrong winding than to rewind downstream. GDAL's GeoJSON driver has an RFC7946 creation option that is off by default, and PostGIS has a function that produces counterclockwise exteriors. Its name is ST_ForcePolygonCCW, not ST_ForceRHR, whose name PostGIS's own docs warn against.

GDAL's GeoJSON driver documentation states that RFC7946 defaults to NO, meaning legacy GeoJSON 2008 output. Set it to YES and "Polygons will be written such as to follow the right-hand rule for orientation (counterclockwise external rings, clockwise internal rings)":

ogr2ogr -f GeoJSON out.geojson in.gpkg -lco RFC7946=YES

That single flag changes more than winding. RFC7946=YES also forces CRS84 output, splits geometries at the antimeridian, and drops coordinate precision to seven decimal digits by default. GDAL's own RFC 65 design document lists counter-clockwise winding among the significant differences between the two modes. The CRS84 part matters if your source data is in a projected system, because the reprojection is not optional; EPSG codes explained covers what that identifier actually pins down.

From the database side, use the function whose name says the orientation rather than the one whose name says "RHR":

-- Emit RFC 7946-compliant GeoJSON straight from PostGIS
SELECT ST_AsGeoJSON(ST_ForcePolygonCCW(geom)) AS geojson
FROM parcels;

-- Or normalize the stored geometry once
UPDATE parcels
SET geom = ST_ForcePolygonCCW(geom);

ST_ForcePolygonCCW forces counter-clockwise exterior rings and clockwise interior rings, matching RFC 7946, and has been available since PostGIS 2.4.0. Its mirror, ST_ForcePolygonCW, does the reverse.

Winding order is not the same thing as a valid polygon

Rewinding reverses the order of a ring's vertices. It does not move them, so it cannot repair a self-intersecting ring, an unclosed ring, or a hole that sits outside its exterior. Validators report those separately from winding order, and a polygon can pass the right-hand-rule check while remaining geometrically broken.

The two concerns are independent in both directions. A polygon can follow the right-hand rule perfectly and still be invalid because its boundary crosses itself, and a self-consistent, non-self-intersecting polygon can be wound backwards. The geojson-invalid-geometry write-up separates them for exactly this reason. Most tooling does not: geojsonhint has no self-intersection check at all, so the geojsonlint.com front end built on it cannot report one, and a file that passes there may still be geometrically broken.

Two panels. On the left a clean four-sided ring wound clockwise, whose vertex list A, B, C, D is reversed to A, D, C, B to become compliant. On the right the same four points listed so that two edges cross, forming a bowtie that reversal leaves exactly as it was.

Python's geojson-validator makes the split visible in its API. It names the winding criteria exterior_not_ccw and interior_not_cw, and its fix_geometries() function always repairs unclosed, exterior_not_ccw and interior_not_cw even when you did not ask for those criteria. Everything else is opt-in, which tells you which class of problem the maintainers consider mechanically safe to fix.

from geojson_validator import fix_geometries

fixed = fix_geometries(gj)  # always rewinds and closes rings

One place where you might expect winding to change a computed result, it does not. PostGIS ticket #5238 asked why ST_Area() on a geography value ignores the coordinate winding order. It was closed as wontfix, with the maintainer's answer that geography "doesn't care about winding order and always computes based on short-test edge", and so "does not support correctly calculate measures of geometries larger than a half-hemisphere". Rewinding will not change that number.

FAQ

Does the right-hand rule affect ST_Area in PostGIS?

Not on geography. Ticket #5238 asked exactly this and was closed as wontfix: geography ignores winding order entirely and always computes from the shorter edge, which is why it cannot correctly measure a polygon larger than half a hemisphere.

Does Mapbox require the right-hand rule?

Mapbox GL Draw does, in some versions. A counterclockwise-drawn polygon passed to Draw.add() was reported to throw the literal right-hand-rule error in v0.11.18 and later, while v0.11.15 accepted it. Separately, @mapbox/geojsonhint reports the violation by default, which is where most people first meet the message, but it exposes ignoreRightHandRule to switch the check off.

Is a wrongly wound polygon invalid GeoJSON?

It depends which side of the pipe you are on. RFC 7946 makes the orientation a normative MUST for producers, and in the same section tells parsers they SHOULD NOT reject polygons that break it, for backwards compatibility with the 2008 specification that never mentioned winding. Fix it when you publish; do not assume a file you received is corrupt.

Which PostGIS function should I use to fix winding for GeoJSON?

ST_ForcePolygonCCW, not ST_ForceRHR. The two names describe opposite results: PostGIS's right-hand rule puts the bounded area on the right, which winds exteriors clockwise, the mirror of what RFC 7946 asks for. The PostGIS documentation warns about the clash in its own reference and recommends the explicitly named functions instead.

Does rewinding fix an invalid polygon?

No. Rewinding reverses the order a ring's vertices are listed in, and reversal cannot move a vertex. A self-intersecting boundary, an unclosed ring, or a hole sitting outside its exterior all survive it unchanged. Those are separate findings with separate fixes, and a polygon can pass the winding check while failing every one of them.