Skip to main content

GeoJSON Validator & Inspector API

REAL DATA

Validate GeoJSON against RFC 7946: the exact clause behind an unclosed ring, a swapped lat/lng, clockwise winding or a wrong bbox — plus inspect and rewind.

Authoritative reference data or standards computation

Base URL
/api/geojson
Capabilities
4 routes
Last updated
July 30, 2026
Data sources · 4
+1 more
  • Chamberlain & Duquette, Some Algorithms for Polygons on a Sphere (JPL Publication 07-03, June 2007) — spherical signed-area formula

GET /api/geojson/validate

Live requestRuns against the public API
No key required
GET/api/geojson/validate?value=%7B%22type%22%3A%22FeatureCollection%22%2C%22features%22%3A%5B%7B%22type%22%3A%22Feature%22%2C%22geometry%22%3A%7B%22type%22%3A%22Point%22%2C%22coordinates%22%3A%5B102%2C0.5%5D%7D%2C%22properties%22%3A%7B%22prop0%22%3A%22value0%22%7D%7D%2C%7B%22type%22%3A%22Feature%22%2C%22geometry%22%3A%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B101%2C0%5D%2C%5B101%2C1%5D%2C%5B100%2C1%5D%2C%5B100%2C0%5D%5D%5D%7D%2C%22properties%22%3A%7B%22prop1%22%3A%7B%22this%22%3A%22that%22%7D%7D%7D%5D%7D

Request parameters

The GeoJSON text itself — this endpoint never fetches a URL. Capped at 4000 characters because it has to fit in a GET query string; larger input is rejected with a 400, never truncated into a wrong verdict. Percent-encode '+' as %2B: a bare '+' in a query string decodes to a space, which corrupts exponent notation such as 1e+2.

Lowest severity to include in 'findings'. 'warning' (the default) returns errors and warnings; 'note' adds the informational findings; 'error' returns errors only. 'errorCount', 'warningCount' and 'noteCount' are always computed over every finding, before this filter and before the 200-finding cap — the filter changes what you read, never what was found.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/geojson/validate?value=%7B%22type%22%3A%22FeatureCollection%22%2C%22features%22%3A%5B%7B%22type%22%3A%22Feature%22%2C%22geometry%22%3A%7B%22type%22%3A%22Point%22%2C%22coordinates%22%3A%5B102%2C0.5%5D%7D%2C%22properties%22%3A%7B%22prop0%22%3A%22value0%22%7D%7D%2C%7B%22type%22%3A%22Feature%22%2C%22geometry%22%3A%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B101%2C0%5D%2C%5B101%2C1%5D%2C%5B100%2C1%5D%2C%5B100%2C0%5D%5D%5D%7D%2C%22properties%22%3A%7B%22prop1%22%3A%7B%22this%22%3A%22that%22%7D%7D%7D%5D%7D"
const res = await fetch("https://randomapi.dev/api/geojson/validate?value=%7B%22type%22%3A%22FeatureCollection%22%2C%22features%22%3A%5B%7B%22type%22%3A%22Feature%22%2C%22geometry%22%3A%7B%22type%22%3A%22Point%22%2C%22coordinates%22%3A%5B102%2C0.5%5D%7D%2C%22properties%22%3A%7B%22prop0%22%3A%22value0%22%7D%7D%2C%7B%22type%22%3A%22Feature%22%2C%22geometry%22%3A%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B101%2C0%5D%2C%5B101%2C1%5D%2C%5B100%2C1%5D%2C%5B100%2C0%5D%5D%5D%7D%2C%22properties%22%3A%7B%22prop1%22%3A%7B%22this%22%3A%22that%22%7D%7D%7D%5D%7D");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/geojson/validate?value=%7B%22type%22%3A%22FeatureCollection%22%2C%22features%22%3A%5B%7B%22type%22%3A%22Feature%22%2C%22geometry%22%3A%7B%22type%22%3A%22Point%22%2C%22coordinates%22%3A%5B102%2C0.5%5D%7D%2C%22properties%22%3A%7B%22prop0%22%3A%22value0%22%7D%7D%2C%7B%22type%22%3A%22Feature%22%2C%22geometry%22%3A%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B101%2C0%5D%2C%5B101%2C1%5D%2C%5B100%2C1%5D%2C%5B100%2C0%5D%5D%5D%7D%2C%22properties%22%3A%7B%22prop1%22%3A%7B%22this%22%3A%22that%22%7D%7D%7D%5D%7D").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/geojson/validate?value=%7B%22type%22%3A%22FeatureCollection%22%2C%22features%22%3A%5B%7B%22type%22%3A%22Feature%22%2C%22geometry%22%3A%7B%22type%22%3A%22Point%22%2C%22coordinates%22%3A%5B102%2C0.5%5D%7D%2C%22properties%22%3A%7B%22prop0%22%3A%22value0%22%7D%7D%2C%7B%22type%22%3A%22Feature%22%2C%22geometry%22%3A%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B101%2C0%5D%2C%5B101%2C1%5D%2C%5B100%2C1%5D%2C%5B100%2C0%5D%5D%5D%7D%2C%22properties%22%3A%7B%22prop1%22%3A%7B%22this%22%3A%22that%22%7D%7D%7D%5D%7D"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 2
value string required

The GeoJSON text itself — this endpoint never fetches a URL. Capped at 4000 characters because it has to fit in a GET query string; larger input is rejected with a 400, never truncated into a wrong verdict. Percent-encode '+' as %2B: a bare '+' in a query string decodes to a space, which corrupts exponent notation such as 1e+2.

allowed: ≤ 4000
example: value={"type":"Point","coordinates":[-0.1276,51.5072]}
minSeverity enum

Lowest severity to include in 'findings'. 'warning' (the default) returns errors and warnings; 'note' adds the informational findings; 'error' returns errors only. 'errorCount', 'warningCount' and 'noteCount' are always computed over every finding, before this filter and before the 200-finding cap — the filter changes what you read, never what was found.

default: warning
allowed: note | warning | error
example: minSeverity=note
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=valid,conformant
exclude list

Return all fields except these (comma-separated).

example: exclude=findings
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 9
valid boolean

No 'error' findings — a conforming RFC 7946 parser must not reject this document.

example: true

conformant boolean

No finding whose 'conformance' is must, should or descriptive — the document breaks nothing RFC 7946 states. A clockwise exterior ring makes this false while 'valid' stays true.

example: false

type string nullable

The document's top-level 'type' member when it is one of the nine GeoJSON types; null when absent, wrong-typed or unrecognised.

example: FeatureCollection

errorCount integer

Total 'error' findings, computed before 'minSeverity' and before the 200-finding cap.

example: 0

warningCount integer

Total 'warning' findings, computed before 'minSeverity' and before the 200-finding cap.

example: 1

noteCount integer

Total 'note' findings, computed before 'minSeverity' and before the 200-finding cap.

example: 0

findingCount integer

Findings actually present in 'findings' after the 'minSeverity' filter and the cap.

example: 1

truncated boolean

True when more than 200 findings survived the filter and the list was cut; a meta.warnings entry says how many were produced.

example: false

findings object[]

Ordered findings: the error group first, then warnings, then notes, each group in walk order. Every entry has 'severity' (error|warning|note), 'code' (a stable identifier enumerated by /checks), 'rule' (e.g. "RFC 7946 §3.1.6", or null when the finding is this endpoint's own diagnosis), 'ruleUrl' (a deep link to that section, or null), 'conformance' (must|should|descriptive|permitted|null), 'message' (one sentence naming the clause), 'path' (an RFC 6901 JSON Pointer to the offending value; "" is the document root) and 'value'. 'value' carries the offending value only when it is a finite number, a string of at most 64 characters, or an array of at most four finite numbers — otherwise it is null. Finite numbers are echoed verbatim and never rounded, so 'value' always matches the number the message names. A number that parsed to Infinity or -Infinity (the 1e400 case) is always reported as null, never as the parsed value.

example: [{"severity":"warning","code":"ring-winding-not-right-hand","rule":"RFC 7946 §3.1.6","ruleUrl":"https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1.6","conformance":"must","message":"The exterior ring is clockwise; RFC 7946 §3.1.6 requires the right-hand rule (exterior rings counterclockwise, holes clockwise). The same section tells parsers not to reject Polygons that do not follow it, so this is a warning, not an error — call /api/geojson/rewind to fix it.","path":"/coordinates/0","value":null}]

Documented examples

Build-generated requests and complete responses
3
A clean FeatureCollection
GET /api/geojson/validate?value=%7B%22type%22%3A%22FeatureCollection%22%2C%22features%22%3A%5B%7B%22type%22%3A%22Feature%22%2C%22geometry%22%3A%7B%22type%22%3A%22Point%22%2C%22coordinates%22%3A%5B102%2C0.5%5D%7D%2C%22properties%22%3A%7B%22prop0%22%3A%22value0%22%7D%7D%2C%7B%22type%22%3A%22Feature%22%2C%22geometry%22%3A%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B101%2C0%5D%2C%5B101%2C1%5D%2C%5B100%2C1%5D%2C%5B100%2C0%5D%5D%5D%7D%2C%22properties%22%3A%7B%22prop1%22%3A%7B%22this%22%3A%22that%22%7D%7D%7D%5D%7D
{
  "data": {
    "valid": true,
    "conformant": true,
    "type": "FeatureCollection",
    "errorCount": 0,
    "warningCount": 0,
    "noteCount": 0,
    "findingCount": 0,
    "truncated": false,
    "findings": []
  },
  "meta": {
    "endpoint": "geojson",
    "route": "/validate",
    "params": {
      "value": "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[102,0.5]},\"properties\":{\"prop0\":\"value0\"}},{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[100,0],[101,0],[101,1],[100,1],[100,0]]]},\"properties\":{\"prop1\":{\"this\":\"that\"}}}]}",
      "minSeverity": "warning"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Clockwise exterior ring — a warning, never an error
GET /api/geojson/validate?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B100%2C1%5D%2C%5B101%2C1%5D%2C%5B101%2C0%5D%2C%5B100%2C0%5D%5D%5D%7D
{
  "data": {
    "valid": true,
    "conformant": false,
    "type": "Polygon",
    "errorCount": 0,
    "warningCount": 1,
    "noteCount": 0,
    "findingCount": 1,
    "truncated": false,
    "findings": [
      {
        "severity": "warning",
        "code": "ring-winding-not-right-hand",
        "rule": "RFC 7946 §3.1.6",
        "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1.6",
        "conformance": "must",
        "message": "The exterior ring is clockwise; RFC 7946 §3.1.6 requires the right-hand rule (exterior rings counterclockwise, holes clockwise). The same section tells parsers not to reject Polygons that do not follow it, so this is a warning, not an error — call /api/geojson/rewind to fix it.",
        "path": "/coordinates/0",
        "value": null
      }
    ]
  },
  "meta": {
    "endpoint": "geojson",
    "route": "/validate",
    "params": {
      "value": "{\"type\":\"Polygon\",\"coordinates\":[[[100,0],[100,1],[101,1],[101,0],[100,0]]]}",
      "minSeverity": "warning"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Swapped latitude and longitude
GET /api/geojson/validate?value=%7B%22type%22%3A%22Point%22%2C%22coordinates%22%3A%5B10%2C175%5D%7D&minSeverity=note
{
  "data": {
    "valid": false,
    "conformant": false,
    "type": "Point",
    "errorCount": 1,
    "warningCount": 1,
    "noteCount": 0,
    "findingCount": 2,
    "truncated": false,
    "findings": [
      {
        "severity": "error",
        "code": "latitude-out-of-range",
        "rule": "RFC 7946 §4",
        "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-4",
        "conformance": "descriptive",
        "message": "Latitude 175 is outside -90 to 90, so it names no point in the WGS 84 coordinate reference system RFC 7946 §4 fixes for all GeoJSON coordinates.",
        "path": "/coordinates",
        "value": [
          10,
          175
        ]
      },
      {
        "severity": "warning",
        "code": "suspected-swapped-position",
        "rule": "RFC 7946 §3.1.1",
        "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1.1",
        "conformance": null,
        "message": "This position is out of range as written but in range as [175, 10] — RFC 7946 §3.1.1 requires longitude before latitude, so the two elements look swapped.",
        "path": "/coordinates",
        "value": [
          10,
          175
        ]
      }
    ]
  },
  "meta": {
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/geojson/inspect

Refuses a document with any RFC 7946 error (400 → /validate), because area, length and winding computed over a malformed ring are noise. Warnings and notes do not block inspection.

Live requestRuns against the public API
No key required
GET/api/geojson/inspect?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B101%2C0%5D%2C%5B101%2C1%5D%2C%5B100%2C1%5D%2C%5B100%2C0%5D%5D%5D%7D

Request parameters

The GeoJSON text itself — this endpoint never fetches a URL. Capped at 4000 characters because it has to fit in a GET query string; larger input is rejected with a 400, never truncated into a wrong verdict. Percent-encode '+' as %2B: a bare '+' in a query string decodes to a space, which corrupts exponent notation such as 1e+2.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/geojson/inspect?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B101%2C0%5D%2C%5B101%2C1%5D%2C%5B100%2C1%5D%2C%5B100%2C0%5D%5D%5D%7D"
const res = await fetch("https://randomapi.dev/api/geojson/inspect?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B101%2C0%5D%2C%5B101%2C1%5D%2C%5B100%2C1%5D%2C%5B100%2C0%5D%5D%5D%7D");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/geojson/inspect?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B101%2C0%5D%2C%5B101%2C1%5D%2C%5B100%2C1%5D%2C%5B100%2C0%5D%5D%5D%7D").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/geojson/inspect?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B101%2C0%5D%2C%5B101%2C1%5D%2C%5B100%2C1%5D%2C%5B100%2C0%5D%5D%5D%7D"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 1
value string required

The GeoJSON text itself — this endpoint never fetches a URL. Capped at 4000 characters because it has to fit in a GET query string; larger input is rejected with a 400, never truncated into a wrong verdict. Percent-encode '+' as %2B: a bare '+' in a query string decodes to a space, which corrupts exponent notation such as 1e+2.

allowed: ≤ 4000
example: value={"type":"Point","coordinates":[-0.1276,51.5072]}
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=type,objectCounts
exclude list

Return all fields except these (comma-separated).

example: exclude=radiusMeters
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 23
type string

The document's top-level GeoJSON type. Always present, because an unresolvable type is an error and errors are refused.

example: FeatureCollection

objectCounts object

One key per GeoJSON type — Feature, FeatureCollection, Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection — always all nine present, zeros included. Geometries nested inside GeometryCollections and Features are counted.

example: {"Point":1,"MultiPoint":0,"LineString":0,"MultiLineString":0,"Polygon":1,"MultiPolygon":0,"GeometryCollection":0,"Feature":2,"FeatureCollection":1}

positionCount integer

Total positions across every geometry, counting each ring's repeated closing position every time it appears.

example: 6

ringCount integer

Linear rings across all Polygons and MultiPolygons, exterior and interior.

example: 1

unlocatedFeatureCount integer

Features whose 'geometry' is null. RFC 7946 §3.2 calls these unlocated; they are legal and render nothing.

example: 0

emptyGeometryCount integer

Geometries with an empty 'coordinates' array. RFC 7946 §3.1 lets processors treat these as null objects, so they can silently vanish.

example: 0

hasAltitude boolean

At least one position carries the optional third element, which RFC 7946 §4 defines as height in metres above or below the WGS 84 reference ellipsoid.

example: false

mixedDimensions boolean

True when some positions have two elements and others three. Legal, but it makes RFC 7946 §5's 2*n bbox length ambiguous.

example: false

elevationRange object nullable

{ minMeters, maxMeters } over the third elements present, rounded to 3 decimals; null when no position has one.

example: null

bbox object nullable

Computed bounding box as { array, west, south, east, north }. 'array' is the ready-to-paste RFC 7946 §5 member, always the four-element two-dimensional form [west, south, east, north], rounded to 9 decimals; the named members repeat it so nothing depends on positional order. The 2-D form is emitted even for 3-D documents, because §5's 2*n rule is ambiguous for a document that mixes 2- and 3-element positions; the height extent is reported separately as 'elevationRange'. It is the extent of the positions themselves and nothing more: when a ring encircles a pole the box does not cover the polar cap that ring bounds, because §5.3's spherical-cap form ([-180, minlat, 180, 90]) is an inference about intent rather than a measurement — a meta.warnings entry says so, and the ring's 'winding' and 'areaSquareMeters' are refused for the same reason. null when the document contains no position.

example: {"array":[100,0,102,1],"west":100,"south":0,"east":102,"north":1}

bboxCrossesAntimeridian boolean

True when the chosen longitude interval wraps ±180, in which case west is greater than east exactly as RFC 7946 §5.2 requires. A meta.warnings entry says so in words.

example: false

bboxAlternative object nullable

The rejected candidate longitude interval, in the same shape as 'bbox'. Of the two intervals covering every position — the plain min→max interval and the one that wraps the antimeridian — this endpoint emits the narrower and hands you the other, because for a sparse global point set the narrower interval can be the wrong intent. On an exact tie (longitudes -179 and 1, say, where both intervals span 180 degrees) the plain box wins and the wrapped one is still disclosed here, because equally narrow is not the same as identical. null only when there is a single candidate — when the widest gap between consecutive longitudes is the gap across ±180 itself, so the minimal covering interval already is the plain one.

example: null

bboxCenter object nullable

{ longitude, latitude } — the centre of the emitted box, longitude normalised to [-180, 180). Not a centre of mass, not an area centroid, not the mean of the positions. null when 'bbox' is null.

example: {"longitude":101,"latitude":0.5}

declaredBbox object nullable

The document's own root-level 'bbox' member as { array, dimensions, west, south, east, north }. 'array' is the member verbatim — unrounded, and either 4 or 6 elements — and 'dimensions' is §5's n, so a 3-D box keeps its height axes; the named edges are read from the §5 positions (indices 0, 1 and 3, 4 for the 6-element form). null when the member is absent, or when it is unusable: not an array, not length 4 or 6, or carrying an element that parsed to a non-finite number. Nested 'bbox' members on Features and Geometries are legal under §5 and are shape-checked by /validate, but are not read here.

example: null

declaredBboxContainsGeometry boolean nullable

Whether the declared root bbox contains every position. Antimeridian-aware: when its west exceeds its east the longitude interval is [west, 180] plus [-180, east], per RFC 7946 §5.2. Elevation-aware: RFC 7946 §5 says a bbox's edges follow lines of constant longitude, latitude and elevation, so a six-element box's height axes (indices 2 and 5, read as the interval they span) are also compared against every position that carries a third element — a four-element box declares no height extent, and a two-element position carries none, so neither is compared. null when there is no usable declared bbox.

example: null

lengthMeters float nullable

Great-circle length of every LineString and MultiLineString, summed, with no connecting segment between the parts of a MultiLineString. Rounded to 3 decimals; null when the document has none.

example: null

perimeterMeters float nullable

Great-circle length of every linear ring, exterior and interior, summed. Rounded to 3 decimals; null when the document has no ring.

example: 444763.385

areaSquareMeters float nullable

Spherical area of every Polygon (exterior minus its holes), summed, rounded to 2 decimals. null when there is no polygon, or when some polygon's area cannot be computed — because its holes enclose at least as much area as its exterior, or because one of its rings encircles a pole. A wrong number would be worse than none.

example: 12363718145.18

rings object[]

Up to 100 entries in document order, each with 'path' (an RFC 6901 pointer to the ring array), 'role' (exterior|interior), 'positionCount', 'closed', 'winding' (counterclockwise|clockwise|indeterminate), 'followsRightHandRule' (boolean, or null when the winding is indeterminate) and 'areaSquareMeters' (a non-negative float, or null when indeterminate). Beyond 100 rings a meta.warnings entry says how many were omitted.

example: [{"path":"/features/1/geometry/coordinates/0","role":"exterior","positionCount":5,"closed":true,"winding":"counterclockwise","followsRightHandRule":true,"areaSquareMeters":12363718145.18}]

foreignMemberPaths string[]

Up to 50 RFC 6901 pointers to members RFC 7946 §6.1 does not define. Descendants of a foreign member are not listed separately, and values inside a Feature's 'properties' are not foreign members at all. A structural member on a type §7.1 does not forbid it on — 'coordinates' on a GeometryCollection, 'geometries' on any other Geometry — is listed here rather than invented as an error, because §3.1.8 says a GeometryCollection simply has no 'coordinates' member; the ones §7.1 does forbid are reported by /validate as errors instead.

example: []

crsMemberPresent boolean

True when a 'crs' member is present on any GeoJSON object in the document. RFC 7946 §4's note records that alternative coordinate reference systems were removed from this version, so 'crs' is now a foreign member that some consumers may still act on; this endpoint never honours it.

example: false

model string

Always "mean-earth-sphere" — the same surface /api/geodesy documents. Not an ellipsoid, not survey precision.

example: mean-earth-sphere

radiusMeters float

The IUGG mean Earth radius used for every area and length, in metres.

example: 6371008.8

Documented examples

Build-generated requests and complete responses
3
Measure the RFC 7946 example polygon
GET /api/geojson/inspect?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B101%2C0%5D%2C%5B101%2C1%5D%2C%5B100%2C1%5D%2C%5B100%2C0%5D%5D%5D%7D
{
  "data": {
    "type": "Polygon",
    "objectCounts": {
      "Point": 0,
      "MultiPoint": 0,
      "LineString": 0,
      "MultiLineString": 0,
      "Polygon": 1,
      "MultiPolygon": 0,
      "GeometryCollection": 0,
      "Feature": 0,
      "FeatureCollection": 0
    },
    "positionCount": 5,
    "ringCount": 1,
    "unlocatedFeatureCount": 0,
    "emptyGeometryCount": 0,
    "hasAltitude": false,
    "mixedDimensions": false,
    "elevationRange": null,
    "bbox": {
      "array": [
        100,
        0,
        101,
        1
      ],
      "west": 100,
      "south": 0,
      "east": 101,
      "north": 1
    },
    "bboxCrossesAntimeridian": false,
    "bboxAlternative": null,
    "bboxCenter": {
      "longitude": 100.5,
      "latitude": 0.5
    },
    "declaredBbox": null,
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
A bounding box that wraps the antimeridian
GET /api/geojson/inspect?value=%7B%22type%22%3A%22MultiPoint%22%2C%22coordinates%22%3A%5B%5B177.5%2C-19%5D%2C%5B179.9%2C-17%5D%2C%5B-178.5%2C-16.5%5D%5D%7D
{
  "data": {
    "type": "MultiPoint",
    "objectCounts": {
      "Point": 0,
      "MultiPoint": 1,
      "LineString": 0,
      "MultiLineString": 0,
      "Polygon": 0,
      "MultiPolygon": 0,
      "GeometryCollection": 0,
      "Feature": 0,
      "FeatureCollection": 0
    },
    "positionCount": 3,
    "ringCount": 0,
    "unlocatedFeatureCount": 0,
    "emptyGeometryCount": 0,
    "hasAltitude": false,
    "mixedDimensions": false,
    "elevationRange": null,
    "bbox": {
      "array": [
        177.5,
        -19,
        -178.5,
        -16.5
      ],
      "west": 177.5,
      "south": -19,
      "east": -178.5,
      "north": -16.5
    },
    "bboxCrossesAntimeridian": true,
    "bboxAlternative": {
      "array": [
        -178.5,
        -19,
        179.9,
        -16.5
    …
    ]
  }
}
A line carrying altitude
GET /api/geojson/inspect?value=%7B%22type%22%3A%22LineString%22%2C%22coordinates%22%3A%5B%5B0%2C0%2C100%5D%2C%5B1%2C0%2C250%5D%5D%7D
{
  "data": {
    "type": "LineString",
    "objectCounts": {
      "Point": 0,
      "MultiPoint": 0,
      "LineString": 1,
      "MultiLineString": 0,
      "Polygon": 0,
      "MultiPolygon": 0,
      "GeometryCollection": 0,
      "Feature": 0,
      "FeatureCollection": 0
    },
    "positionCount": 2,
    "ringCount": 0,
    "unlocatedFeatureCount": 0,
    "emptyGeometryCount": 0,
    "hasAltitude": true,
    "mixedDimensions": false,
    "elevationRange": {
      "minMeters": 100,
      "maxMeters": 250
    },
    "bbox": {
      "array": [
        0,
        0,
        1,
        0
      ],
      "west": 0,
      "south": 0,
      "east": 1,
      "north": 0
    },
    "bboxCrossesAntimeridian": false,
    "bboxAlternative": null,
    "bboxCenter": {
      "longitude": 0.5,
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/geojson/rewind

Rewinding is position-order reversal of a ring array and nothing else. Rings are only found at the 'coordinates' of a Polygon or MultiPolygon reached through 'features[].geometry', 'geometries[]' or the root geometry — never inside 'properties' or a foreign member (RFC 7946 §6.1).

Live requestRuns against the public API
No key required
GET/api/geojson/rewind?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B100%2C1%5D%2C%5B101%2C1%5D%2C%5B101%2C0%5D%2C%5B100%2C0%5D%5D%5D%7D

Request parameters

The GeoJSON text itself — this endpoint never fetches a URL. Capped at 4000 characters because it has to fit in a GET query string; larger input is rejected with a 400, never truncated into a wrong verdict. Percent-encode '+' as %2B: a bare '+' in a query string decodes to a space, which corrupts exponent notation such as 1e+2.

'right-hand' is RFC 7946 §3.1.6: exterior rings counterclockwise, interior rings clockwise. 'left-hand' is the exact inverse — not RFC 7946, but the 2008 draft left winding undefined and some older renderers and d3 pipelines expect it. The applied convention is echoed in the 'convention' field.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/geojson/rewind?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B100%2C1%5D%2C%5B101%2C1%5D%2C%5B101%2C0%5D%2C%5B100%2C0%5D%5D%5D%7D"
const res = await fetch("https://randomapi.dev/api/geojson/rewind?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B100%2C1%5D%2C%5B101%2C1%5D%2C%5B101%2C0%5D%2C%5B100%2C0%5D%5D%5D%7D");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/geojson/rewind?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B100%2C1%5D%2C%5B101%2C1%5D%2C%5B101%2C0%5D%2C%5B100%2C0%5D%5D%5D%7D").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/geojson/rewind?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B100%2C1%5D%2C%5B101%2C1%5D%2C%5B101%2C0%5D%2C%5B100%2C0%5D%5D%5D%7D"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 2
value string required

The GeoJSON text itself — this endpoint never fetches a URL. Capped at 4000 characters because it has to fit in a GET query string; larger input is rejected with a 400, never truncated into a wrong verdict. Percent-encode '+' as %2B: a bare '+' in a query string decodes to a space, which corrupts exponent notation such as 1e+2.

allowed: ≤ 4000
example: value={"type":"Point","coordinates":[-0.1276,51.5072]}
winding enum

'right-hand' is RFC 7946 §3.1.6: exterior rings counterclockwise, interior rings clockwise. 'left-hand' is the exact inverse — not RFC 7946, but the 2008 draft left winding undefined and some older renderers and d3 pipelines expect it. The applied convention is echoed in the 'convention' field.

default: right-hand
allowed: right-hand | left-hand
example: winding=left-hand
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=convention,changed
exclude list

Return all fields except these (comma-separated).

example: exclude=document
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 7
convention string

The applied convention, right-hand or left-hand, echoed so a stored response is self-describing.

example: right-hand

changed boolean

True when at least one ring was reversed.

example: true

ringCount integer

Linear rings examined.

example: 1

rewoundRingCount integer

Rings whose position order was reversed.

example: 1

indeterminateRingCount integer

Rings left untouched because their winding could not be determined — pole-encircling or zero-area.

example: 0

rewoundRings object[]

Up to 100 entries in document order, only the rings that changed: 'path' (an RFC 6901 pointer), 'role' (exterior|interior), 'windingBefore' and 'windingAfter'.

example: [{"path":"/coordinates/0","role":"exterior","windingBefore":"clockwise","windingAfter":"counterclockwise"}]

document object

The document with its rings rewound. Member order within every object, every foreign member, every 'properties' value, every 'id' and any declared 'bbox' are preserved — reversing a ring does not move a position, so the bbox is still correct. Because the document is parsed and re-serialised, numeric literals are normalised by the JSON round trip (1.0 becomes 1, 1e2 becomes 100) and the original whitespace is not preserved.

example: {"type":"Polygon","coordinates":[[[100,0],[101,0],[101,1],[100,1],[100,0]]]}

Documented examples

Build-generated requests and complete responses
3
Rewind a clockwise exterior ring
GET /api/geojson/rewind?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B100%2C1%5D%2C%5B101%2C1%5D%2C%5B101%2C0%5D%2C%5B100%2C0%5D%5D%5D%7D
{
  "data": {
    "convention": "right-hand",
    "changed": true,
    "ringCount": 1,
    "rewoundRingCount": 1,
    "indeterminateRingCount": 0,
    "rewoundRings": [
      {
        "path": "/coordinates/0",
        "role": "exterior",
        "windingBefore": "clockwise",
        "windingAfter": "counterclockwise"
      }
    ],
    "document": {
      "type": "Polygon",
      "coordinates": [
        [
          [
            100,
            0
          ],
          [
            101,
            0
          ],
          [
            101,
            1
          ],
          [
            100,
            1
          ],
          [
            100,
            0
          ]
        ]
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Apply the legacy left-hand convention
GET /api/geojson/rewind?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B100%2C1%5D%2C%5B101%2C1%5D%2C%5B101%2C0%5D%2C%5B100%2C0%5D%5D%5D%7D&winding=left-hand
{
  "data": {
    "convention": "left-hand",
    "changed": false,
    "ringCount": 1,
    "rewoundRingCount": 0,
    "indeterminateRingCount": 0,
    "rewoundRings": [],
    "document": {
      "type": "Polygon",
      "coordinates": [
        [
          [
            100,
            0
          ],
          [
            100,
            1
          ],
          [
            101,
            1
          ],
          [
            101,
            0
          ],
          [
            100,
            0
          ]
        ]
      ]
    }
  },
  "meta": {
    "endpoint": "geojson",
    "route": "/rewind",
    "params": {
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Already right-handed — nothing changes
GET /api/geojson/rewind?value=%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B100%2C0%5D%2C%5B101%2C0%5D%2C%5B101%2C1%5D%2C%5B100%2C1%5D%2C%5B100%2C0%5D%5D%5D%7D
{
  "data": {
    "convention": "right-hand",
    "changed": false,
    "ringCount": 1,
    "rewoundRingCount": 0,
    "indeterminateRingCount": 0,
    "rewoundRings": [],
    "document": {
      "type": "Polygon",
      "coordinates": [
        [
          [
            100,
            0
          ],
          [
            101,
            0
          ],
          [
            101,
            1
          ],
          [
            100,
            1
          ],
          [
            100,
            0
          ]
        ]
      ]
    }
  },
  "meta": {
    "endpoint": "geojson",
    "route": "/rewind",
    "params": {
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/geojson/checks

The finding catalogue is one exported constant: the walker can only emit a finding through a helper that looks its code up there, and this route returns the same table. Drift between the docs and the behaviour is structurally impossible.

Live requestRuns against the public API
No key required
GET/api/geojson/checks?severity=error

Request parameters

Only checks at this severity.

Only checks at this RFC 7946 conformance level. 'none' selects the checks that are this endpoint's own diagnosis or observation rather than an assertion about a clause — the rows whose 'conformance' is null.

Case-insensitive substring match over 'code' and 'description'.

Advanced response options6 options

Limit the number of returned records (1–100). Defaults to all matches.

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Response format: json envelope, ndjson (one record per line) or csv.

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/geojson/checks?severity=error"
const res = await fetch("https://randomapi.dev/api/geojson/checks?severity=error");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/geojson/checks?severity=error").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/geojson/checks?severity=error"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 3
severity enum

Only checks at this severity.

allowed: error | warning | note
example: severity=error
conformance enum

Only checks at this RFC 7946 conformance level. 'none' selects the checks that are this endpoint's own diagnosis or observation rather than an assertion about a clause — the rows whose 'conformance' is null.

allowed: must | should | descriptive | permitted | none
example: conformance=must
search string

Case-insensitive substring match over 'code' and 'description'.

allowed: 2 – 60
example: search=ring
Universal parameters Shared response and formatting options 6
count int

Limit the number of returned records (1–100). Defaults to all matches.

default: all matches
allowed: 1 – 100
example: count=3
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=code,severity
exclude list

Return all fields except these (comma-separated).

example: exclude=description
format enum

Response format: json envelope, ndjson (one record per line) or csv.

default: json
allowed: json | ndjson | csv
example: format=csv
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 6
code string

Stable kebab-case identifier, the same string /validate puts in 'findings[].code'.

example: ring-winding-not-right-hand

severity string

What this endpoint does about it: error (sets valid=false), warning or note.

example: warning

conformance string nullable

What RFC 7946 itself says: must, should, descriptive (an invariant stated without a keyword) or permitted. null when the check is this endpoint's own diagnosis or an interoperability observation rather than an assertion about a clause.

example: must

rule string nullable

The clause cited, e.g. "RFC 7946 §3.1.6"; null when the check cites none.

example: RFC 7946 §3.1.6

ruleUrl string nullable

Deep link to that section on rfc-editor.org; null when 'rule' is null.

example: https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1.6

description string

One sentence saying what triggers the check.

example: A linear ring's direction is not the right-hand rule for its role. A warning rather than an error because the same section tells parsers not to reject such Polygons.

Documented examples

Build-generated requests and complete responses
3
Every check that fails a document
GET /api/geojson/checks?severity=error
{
  "data": [
    {
      "code": "invalid-json",
      "severity": "error",
      "conformance": "descriptive",
      "rule": "RFC 7946 §2",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-2",
      "description": "The value is not a JSON text, so it cannot be a GeoJSON text."
    },
    {
      "code": "not-a-json-object",
      "severity": "error",
      "conformance": "descriptive",
      "rule": "RFC 7946 §2",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-2",
      "description": "The JSON text parses but is not a JSON object (an array, string, number, boolean or null)."
    },
    {
      "code": "missing-type",
      "severity": "error",
      "conformance": "descriptive",
      "rule": "RFC 7946 §3",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3",
      "description": "An object where a GeoJSON object is required has no 'type' member."
    },
    {
      "code": "type-not-a-string",
      "severity": "error",
      "conformance": "must",
      "rule": "RFC 7946 §3",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3",
      "description": "A 'type' member is present but its value is not a string."
    },
    {
      "code": "unknown-type",
      "severity": "error",
      "conformance": "must",
      "rule": "RFC 7946 §3",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Checks the RFC states as MUST
GET /api/geojson/checks?conformance=must
{
  "data": [
    {
      "code": "type-not-a-string",
      "severity": "error",
      "conformance": "must",
      "rule": "RFC 7946 §3",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3",
      "description": "A 'type' member is present but its value is not a string."
    },
    {
      "code": "unknown-type",
      "severity": "error",
      "conformance": "must",
      "rule": "RFC 7946 §3",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3",
      "description": "A 'type' value is not one of the nine case-sensitive GeoJSON type strings."
    },
    {
      "code": "coordinates-wrong-nesting",
      "severity": "error",
      "conformance": "must",
      "rule": "RFC 7946 §3.1",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1",
      "description": "A 'coordinates' value does not have the array shape RFC 7946 defines for its geometry type — nested too deep, nested too shallow, or a LineString with fewer than two positions. Labelled 'must' because §3.1.6 states the Polygon form with a literal MUST ('the coordinates member MUST be an array of linear ring coordinate arrays'); the other five types state their shape as an invariant."
    },
    {
      "code": "geometry-not-object-or-null",
      "severity": "error",
      "conformance": "must",
      "rule": "RFC 7946 §3.2",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3.2",
      "description": "A Feature's 'geometry' is neither a Geometry object nor a JSON null; §3.2 states this with SHALL."
    },
    {
      "code": "position-too-short",
      "severity": "error",
      "conformance": "must",
      "rule": "RFC 7946 §3.1.1",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1.1",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Linear-ring checks
GET /api/geojson/checks?search=linear%20ring
{
  "data": [
    {
      "code": "coordinates-wrong-nesting",
      "severity": "error",
      "conformance": "must",
      "rule": "RFC 7946 §3.1",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1",
      "description": "A 'coordinates' value does not have the array shape RFC 7946 defines for its geometry type — nested too deep, nested too shallow, or a LineString with fewer than two positions. Labelled 'must' because §3.1.6 states the Polygon form with a literal MUST ('the coordinates member MUST be an array of linear ring coordinate arrays'); the other five types state their shape as an invariant."
    },
    {
      "code": "ring-too-short",
      "severity": "error",
      "conformance": "descriptive",
      "rule": "RFC 7946 §3.1.6",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1.6",
      "description": "A linear ring has fewer than four positions."
    },
    {
      "code": "ring-not-closed",
      "severity": "error",
      "conformance": "must",
      "rule": "RFC 7946 §3.1.6",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1.6",
      "description": "A linear ring's first and last positions do not contain identical values."
    },
    {
      "code": "ring-winding-not-right-hand",
      "severity": "warning",
      "conformance": "must",
      "rule": "RFC 7946 §3.1.6",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1.6",
      "description": "A linear ring's direction is not the right-hand rule for its role. A warning rather than an error because the same section tells parsers not to reject such Polygons."
    },
    {
      "code": "ring-winding-indeterminate",
      "severity": "warning",
      "conformance": null,
      "rule": "RFC 7946 §3.1.6",
      "ruleUrl": "https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1.6",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

About this API

Coverage & behavior

Three things you can do to one GeoJSON document, plus the catalogue of every check behind them — and nothing is ever fetched: you supply the text in ?value=, this endpoint reads no URL and holds no dataset.

  • /validate walks the document and returns one finding per problem, each naming the RFC 7946 clause and the RFC 6901 JSON Pointer of the exact value that caused it.
  • /inspect measures it: geometry counts, a computed bounding box, spherical area and length, and the winding of every ring.
  • /rewind normalises ring winding and tells you precisely which rings changed direction.
  • /checks lists all 50 checks with their severity, their RFC conformance level and the clause they cite.

Severity is not conformance

Every finding carries two orthogonal fields, because conflating them is how validators end up lying.

severity answers what we do about it: error means a conforming RFC 7946 parser is entitled to reject the document, and it sets valid: false; warning means parsers accept it but it will hurt you downstream; note is informational.

conformance answers what the RFC itself says: must, should, descriptive (an invariant the RFC states without a keyword), permitted (the RFC explicitly allows it), or null when the finding is this endpoint's own diagnosis rather than an assertion about a clause.

must marks a check whose strongest trigger violates a clause that literally carries MUST, MUST NOT or SHALL, and only 11 of the 50 checks reach it. Most of RFC 7946's structure is written declaratively — "A GeoJSON Geometry object of any type other than 'GeometryCollection' has a member with the name 'coordinates'" (§3.1) states the invariant without a keyword — so those checks are descriptive. They are still error, and they still set conformant: false; the two fields answer different questions.

Read that as strongest, not every: one check spans two levels. coordinates-wrong-nesting is labelled must because §3.1.6 gives the Polygon form a literal MUST — "the 'coordinates' member MUST be an array of linear ring coordinate arrays" — while the other five geometry types state their shape declaratively ("For type 'LineString', the 'coordinates' member is an array of two or more positions", §3.1.4). A one-position LineString therefore comes back at must too. Its row in /checks says so, and it is the only row that does this.

So a permitted finding can still be a warning — because it is the RFC's own permission, "GeoJSON processors MAY interpret Geometry objects with empty 'coordinates' arrays as null objects" (§3.1), that makes your feature disappear.

Winding is a warning. Always.

§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" — and then, in the same section, "For backwards compatibility, parsers SHOULD NOT reject Polygons that do not follow the right-hand rule." This endpoint therefore never fails a document for winding: a clockwise exterior ring returns valid: true, conformant: false and one ring-winding-not-right-hand warning whose conformance is must. /rewind fixes it.

Latitude and longitude ranges are reported as errors on the same kind of reasoning, stated openly: RFC 7946 fixes the coordinate reference system in §4 rather than writing a numeric MUST for positions, but a latitude of 91 names no point in that CRS, so an error is the honest label.

The 4000-character limit

value is capped at 4000 characters. It is a limit, not a hidden truncation: a longer document is a 400 naming the cap, never a verdict computed on a fragment. The reason is that the document travels in the query string of a GET, and percent-encoding roughly doubles its length — much of a GeoJSON text is { } [ ] " , :, and each of those becomes three characters once encoded. For files past that, run a library locally: @placemarkio/check-geojson, geojson-validation or geojson-rewind.

What this endpoint will not claim

  • No geometric or OGC validity. No self-intersection, ring-nesting, hole-containment or duplicate-ring detection. RFC 7946 never requires a simple geometry, which is exactly why areaSquareMeters is documented as spherical and approximate rather than authoritative.
  • No ellipsoidal measurement. Area and length are computed on the mean-Earth sphere /api/geodesy documents, radius 6,371,008.8 m, imported from one shared constant. Not survey distance, not ellipsoidal area, not terrain.
  • No network access, no CRS database. A crs member is reported as the legacy foreign member it now is, never honoured.
  • No automatic antimeridian cutting. /validate reports a §3.1.9 violation; cutting would mean inserting positions into your data.
  • No generator. This endpoint checks documents you already have; /api/coordinates produces random WGS 84 points.

Why /inspect and /rewind refuse a broken document

Both return a 400 pointing at /validate when the document has any error finding, because area, length and winding computed over a malformed ring are noise, not data. Warnings and notes never block them — a clockwise polygon inspects and rewinds fine. /validate itself never refuses on the content of value: malformed JSON comes back as valid: false with one invalid-json finding, because a validator that will not answer is useless in CI. Its only content-triggered 400 is the 64-level nesting guard, which protects the recursive walk.

Documented limitations

  • Duplicate JSON member names are invisible. JSON.parse keeps the last one and RFC 8259 §4 only says names SHOULD be unique, so this endpoint does not pretend to detect them.
  • /rewind re-serialises. 1.0 becomes 1 and 1e2 becomes 100; whitespace is not preserved. Rewinding itself is nothing but position-order reversal of a ring array.
  • Coordinate-precision notes are measured on the parsed number, on its shortest round-trip decimal form, not on the original literal.
  • A literal that overflows on parse (1e400 becomes Infinity) is reported as a non-number error and is never echoed as a findings[].value or in declaredBbox. Outside coordinates and bbox this endpoint does not police your numbers, and JSON has no way to serialise infinity, so such a value inside properties comes back from /rewind as null.
  • Foreign members are never treated as geometry. §6.1 is explicit that GeoJSON semantics do not apply to foreign members or their descendants, so a clockwise Polygon sitting inside properties is echoed untouched, is not counted and is not rewound.

Every route is deterministic, synchronous and dataset-free: the same query returns the same body.

Use it for

  • Fail a CI job the moment a map fixture stops being RFC 7946-conformant, without installing a geospatial library
  • Find out why a Leaflet or Mapbox layer renders nothing, by getting the RFC clause and the JSON Pointer of the ring that broke it
  • Normalise ring winding before handing a FeatureCollection to a renderer that assumes the right-hand rule

Frequently asked questions

Why does my GeoJSON layer render nothing?

Four causes account for most of it, and /validate names the clause and the JSON Pointer for each: positions written in [latitude, longitude] order (§3.1.1 requires longitude first), an unclosed linear ring (§3.1.6), an empty coordinates array (§3.1 lets processors treat that geometry as a null object), and a Feature whose geometry is null (§3.2 — legal, but unlocated).

Is a clockwise polygon invalid GeoJSON?

No. §3.1.6 requires the right-hand rule and, in the same section, tells parsers not to reject Polygons that break it. So valid stays true, the finding is a warning whose conformance is must, and conformant goes false. Call /rewind to fix the direction.

Does GeoJSON use lat,lng or lng,lat?

Longitude first — RFC 7946 §3.1.1 says the first two elements are longitude and latitude, precisely in that order. A suspected swap is only reported when the position is out of range as written and in range when the two elements are exchanged, never as a guess about a document whose positions are all in range.

How large a GeoJSON document can this endpoint check?

4000 characters, because the document travels in the query string of a GET request. Larger input is a 400 naming the limit — never a truncated verdict. For bigger files, run @placemarkio/check-geojson or geojson-rewind locally.

Does it detect self-intersecting polygons?

No, and it says so. Self-intersection, hole containment and ring nesting are OGC Simple Features validity, not RFC 7946 — which never requires a simple geometry — so this endpoint does not claim to check one.

Standards & references