How to Convert ArcGIS REST JSON to GeoJSON (3 Methods)

By · Last updated May 16, 2026 · 10 min read

Convert ArcGIS REST output to GeoJSON three ways: (1) request f=geojson directly from the layer endpoint, (2) use the @terraformer/arcgis library in Node or browser JavaScript, or (3) run ogr2ogr from GDAL on a saved JSON file.

The f=geojson path is the shortest when the server supports it. If you hit an older endpoint or need to process data programmatically, Terraformer or GDAL handle it without a server change. The format difference matters: ArcGIS REST services return Esri JSON by default, which wraps attributes under "attributes" and encodes ring-based polygon geometry differently than the GeoJSON RFC 7946 standard. Leaflet, MapLibre, and most browser-based web mapping libraries expect RFC 7946 GeoJSON, not Esri JSON.

Esri JSON (f=json) "features": [ { "attributes": { "PIN": "0501401001", "TaxName": "SMITH J" }, "geometry": { "rings": [ [...] ] } } ] GeoJSON (RFC 7946) "features": [ { "properties": { "PIN": "0501401001", "TaxName": "SMITH J" }, "geometry": { "coordinates": [ [...] ] "type": "Polygon" } ]

Method 1: Request GeoJSON Directly with f=geojson

ArcGIS Server 10.4 and later support a f=geojson output format parameter on query endpoints. The server converts Esri JSON to GeoJSON before sending the response. No client-side library needed.

General query pattern:

<serviceUrl>/query?where=1=1&outFields=*&returnGeometry=true&f=geojson&resultRecordCount=10

Working example with Kane County, IL parcels (MapServer with supportsQuery: true):

https://gistech.countyofkane.org/arcgis/rest/services/KanePINList/MapServer/0/query
  ?where=1=1
  &outFields=PIN,TaxName,SiteAddress
  &returnGeometry=true
  &outSR=4326
  &f=geojson
  &resultRecordCount=5

A successful response looks like this (truncated):

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "PIN": "0501401001",
        "TaxName": "SAMPLE OWNER",
        "SiteAddress": "123 MAIN ST"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [[
          [-88.3321, 41.9301],
          [-88.3318, 41.9301],
          [-88.3318, 41.9298],
          [-88.3321, 41.9298],
          [-88.3321, 41.9301]
        ]]
      }
    }
  ]
}

When f=geojson fails, the server returns an error JSON object with a code field rather than a FeatureCollection. The most common cause is an older ArcGIS Server version (pre-10.4) or a service configuration that restricts output formats. In that case, fall back to f=json and convert client-side using Method 2 below.

Note: outSR=4326 is included above to request WGS84 coordinates. See the CRS section below for why this matters.

Method 2: JavaScript with @terraformer/arcgis

When you can't control the server output format, or you're processing data from a mix of endpoint types, @terraformer/arcgis handles the conversion client-side. It works in Node.js and in the browser.

Install:

npm install @terraformer/arcgis

Fetch from an ArcGIS endpoint and convert:

import { arcgisToGeoJSON } from "@terraformer/arcgis";

async function fetchAsGeoJSON(url) {
  const queryUrl = `${url}/query?where=1=1&outFields=*&returnGeometry=true&outSR=4326&f=json`;
  const response = await fetch(queryUrl);
  const esriJson = await response.json();

  // arcgisToGeoJSON accepts either a full FeatureSet or a single feature
  const geojson = arcgisToGeoJSON(esriJson);
  return geojson; // { type: "FeatureCollection", features: [...] }
}

// Example: Kane County parcels
const geojson = await fetchAsGeoJSON(
  "https://gistech.countyofkane.org/arcgis/rest/services/KanePINList/MapServer/0"
);
console.log(geojson.features.length); // number of returned features

Attribute names map directly. attributes.PIN in Esri JSON becomes properties.PIN in GeoJSON. The objectId field (Esri's internal row ID) carries through under the same name.

Geometry type handling by Terraformer:

  • Point{"x": ..., "y": ...} becomes {"type": "Point", "coordinates": [x, y]}. If the source has Z values ("z": ...), the output coordinates are [x, y, z].
  • Polyline — Esri paths arrays become GeoJSON LineString or MultiLineString coordinates.
  • Polygon — Esri rings become GeoJSON Polygon or MultiPolygon. Terraformer handles ring-winding-order differences between the two specs automatically.
  • M values (measure values on linear features) are stripped by default — they have no equivalent in RFC 7946 GeoJSON.

If you use the @urbankitstudio/atlas npm package to look up verified county endpoints, you can chain it directly:

import { findCounty } from "@urbankitstudio/atlas";
import { arcgisToGeoJSON } from "@terraformer/arcgis";

const county = findCounty("IL", "kane");
// county.endpoint = "https://gistech.countyofkane.org/..."

const response = await fetch(
  `${county.endpoint}/query?where=1=1&outFields=*&returnGeometry=true&outSR=4326&f=json&resultRecordCount=50`
);
const esriJson = await response.json();
const geojson = arcgisToGeoJSON(esriJson);

Method 3: ogr2ogr (Command Line, Batch)

ogr2ogr is part of the GDAL toolkit and can convert between most geospatial formats from the command line. For batch exports, ETL pipelines, or converting large files that would strain a browser, it's the right choice.

Install GDAL from gdal.org (the OSGeo4W installer on Windows, brew install gdal on macOS, apt-get install gdal-bin on Debian/Ubuntu).

First, save the ArcGIS REST response to a file:

curl "https://gistech.countyofkane.org/arcgis/rest/services/KanePINList/MapServer/0/query?where=1=1&outFields=*&returnGeometry=true&outSR=4326&f=json&resultRecordCount=1000" \
  -o kane.json

Then convert with ogr2ogr:

ogr2ogr -f GeoJSON kane.geojson kane.json

GDAL recognizes the Esri FeatureSet JSON format automatically. The output is a valid RFC 7946 GeoJSON file. For datasets larger than a single-request page, script the pagination and concatenate:

#!/bin/bash
# Fetch in pages of 1000, merge with ogr2ogr
ENDPOINT="https://gistech.countyofkane.org/arcgis/rest/services/KanePINList/MapServer/0/query"
OFFSET=0
PAGE=1000

while true; do
  curl "${ENDPOINT}?where=1=1&outFields=*&returnGeometry=true&outSR=4326&f=json&resultRecordCount=${PAGE}&resultOffset=${OFFSET}" \
    -o "chunk_${OFFSET}.json"

  COUNT=$(python3 -c "import json,sys; d=json.load(open('chunk_${OFFSET}.json')); print(len(d.get('features',[])))")
  [ "$COUNT" -lt "$PAGE" ] && break
  OFFSET=$((OFFSET + PAGE))
done

# Merge all chunks into one GeoJSON
ogr2ogr -f GeoJSON kane_full.geojson chunk_0.json
for f in chunk_*.json; do
  [ "$f" = "chunk_0.json" ] && continue
  ogr2ogr -f GeoJSON -update -append kane_full.geojson "$f"
done

ogr2ogr is the right tool when: you're integrating with an existing GIS pipeline (PostGIS load, QGIS import, GDAL raster overlays), processing a full county dataset with tens of thousands of parcels, or you need output in multiple formats simultaneously (-f PostgreSQL, -f FlatGeobuf, etc.).

Method Comparison

Method When to use Pros Cons Setup
f=geojson query param Server-side conversion whenever the endpoint supports it No client code or install; single HTTP call Requires ArcGIS Server 10.4+; not all services enable it None
@terraformer/arcgis (JS) Browser or Node apps that need to handle f=json responses programmatically Works on any endpoint regardless of server version; bidirectional (GeoJSON → Esri JSON too) Adds a JS dependency; runs client-side (payload transferred before conversion) npm install @terraformer/arcgis
ogr2ogr (GDAL) Batch exports, large datasets, GIS pipeline integration Handles very large files; supports 80+ output formats; scriptable Requires GDAL install; not suitable for browser or real-time use GDAL install (~100 MB)
QGIS (GUI) One-off conversions for non-coders Point-and-click, no command line; previews geometry before export Manual process; not automatable without PyQGIS scripting QGIS install (~1 GB)

Coordinate Reference System Gotchas

RFC 7946 mandates WGS84 (EPSG:4326) for all GeoJSON. ArcGIS REST services frequently serve data in a different CRS — most commonly Web Mercator (EPSG:3857) or a state-plane projection specific to the county. If you skip the reprojection step, your GeoJSON will parse correctly but the coordinates will be in the wrong units and your features will land in the wrong place on any map that expects longitude/latitude.

The fix is to add outSR=4326 to every ArcGIS REST query that returns geometry. This tells the server to reproject before sending the response:

...&returnGeometry=true&outSR=4326&f=geojson

Kane County's MapServer (Illinois state plane) serves coordinates in EPSG:3435 (Illinois East, US feet) by default. Without outSR=4326, a parcel centroid might come back as [1180000, 1900000] instead of [-88.33, 41.93]. Drop those coordinates into Leaflet and your parcels will render somewhere off the coast of Ghana.

When using ogr2ogr, reproject with -t_srs EPSG:4326:

ogr2ogr -f GeoJSON -t_srs EPSG:4326 kane.geojson kane.json

When using Terraformer, pass outSR=4326 in the fetch URL before converting — Terraformer does not reproject, it only restructures. The coordinate values it converts are exactly what the server sent.

You can verify the CRS of any ArcGIS layer by fetching the service root at ?f=json and looking at the spatialReference field. A wkid of 4326 means the server stores data in WGS84 already (rare for county data); 3857 is Web Mercator; anything in the 2000–4000 range is likely a state-plane projection. For a full list, the EPSG registry at epsg.io covers every projection code.

Frequently Asked Questions

Can I always use f=geojson on an ArcGIS endpoint?

No. The f=geojson parameter works on ArcGIS Server 10.4+ and ArcGIS Online FeatureServer layers. Older MapServer instances and some enterprise deployments only support f=json (Esri JSON). Check the service root at ?f=json — if the capabilities list includes "Query" and the server version is 10.4 or newer, f=geojson almost certainly works.

Is converted GeoJSON identical to the source Esri JSON?

The geometry and coordinates convert faithfully (assuming matching CRS). The attribute structure differs: Esri JSON wraps attributes under an "attributes" key inside each feature object; GeoJSON uses "properties". Field names carry over as-is. The objectId field appears in both, but ring/path geometry gets rewritten to GeoJSON coordinates arrays.

How do I handle large datasets when converting ArcGIS REST to GeoJSON?

ArcGIS REST services cap results per request — commonly 1,000 or 2,000 features. Paginate using resultOffset and resultRecordCount: fetch the first 1,000 with resultOffset=0, then resultOffset=1000, and so on until the response contains fewer features than your page size. With ogr2ogr, use a WHERE clause to split large pulls into manageable chunks.

What does maxAllowableOffset do in an ArcGIS query?

It simplifies geometry server-side before the response leaves. A value of 1 at Web Mercator scale removes vertices closer than 1 map unit apart, cutting payload size for complex parcel polygons. The tradeoff is reduced polygon precision. For display at city or county zoom levels, maxAllowableOffset=5 (meters, in EPSG:3857) is usually imperceptible. For legal-grade geometry, omit it.

Does f=geojson work for cached MapServer layers?

Only if the layer has supportsQuery set to true in its service metadata. Purely cached tile layers have no query endpoint at all — they serve pre-rendered images, not feature data. Check the layer's JSON metadata at the layer URL + ?f=json. If "Query" does not appear in the capabilities string, neither f=json nor f=geojson will return features.

Can I convert back from GeoJSON to ArcGIS (Esri) JSON?

Yes. The @terraformer/arcgis package handles both directions. The geojsonToArcGIS() function converts a GeoJSON FeatureCollection or individual Feature back to Esri JSON — useful when you need to POST features to a FeatureServer's /applyEdits endpoint or feed data to ArcGIS API for JavaScript.

Why does my converted GeoJSON have null geometry?

Three likely causes: (1) the query included returnGeometry=false — check your URL parameters; (2) you hit a geometry-only layer with no attributes, but the parcel-attributes layer is a sibling at a different index (e.g., /MapServer/1 instead of /MapServer/0); (3) the ArcGIS service returned empty geometry for features outside the spatial filter. Fix (1) by adding returnGeometry=true; fix (2) by inspecting the service root at /MapServer?f=json to see all available layers.