Using GeoJSON with PostGIS
Learn how to import export and query GeoJSON data in PostGIS spatial databases.
Why PostGIS and GeoJSON?
PostGIS is the spatial extension for PostgreSQL and one of the most powerful open-source geospatial databases. It stores, indexes, and queries geographic data with hundreds of spatial functions. GeoJSON is the standard format for exchanging that data with web applications, APIs, and mapping libraries.
PostGIS has native GeoJSON support built in — you can convert geometries to and from GeoJSON with single function calls, making it the ideal backend for any GeoJSON-powered application.
Setting Up PostGIS
Installing the Extension
SQL (PostGIS)-- Enable PostGIS on an existing PostgreSQL database
CREATE EXTENSION IF NOT EXISTS postgis;
-- Verify installation
SELECT PostGIS_Full_Version();Creating a Spatial Table
SQL (PostGIS)CREATE TABLE parks (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
park_type TEXT,
area_acres NUMERIC,
geom GEOMETRY(Polygon, 4326) -- WGS 84 coordinate system
);
-- Create a spatial index for fast queries
CREATE INDEX idx_parks_geom ON parks USING GIST (geom);The GEOMETRY(Polygon, 4326) column type constrains the geometry to polygons in WGS 84 (EPSG:4326), which is the same coordinate system GeoJSON uses. Always create a GIST index on geometry columns for performant spatial queries.
Common Geometry Column Types
SQL (PostGIS)-- Points (e.g., locations, POIs)
geom GEOMETRY(Point, 4326)
-- Lines (e.g., roads, trails)
geom GEOMETRY(LineString, 4326)
-- Polygons (e.g., boundaries, parcels)
geom GEOMETRY(Polygon, 4326)
-- Any geometry type
geom GEOMETRY(Geometry, 4326)
-- Multi-geometries
geom GEOMETRY(MultiPolygon, 4326)
geom GEOMETRY(MultiLineString, 4326)
geom GEOMETRY(MultiPoint, 4326)Converting Geometries to GeoJSON
ST_AsGeoJSON — Single Geometry
SQL (PostGIS)-- Convert a geometry column to a GeoJSON geometry string
SELECT name, ST_AsGeoJSON(geom) AS geojson_geometry
FROM parks
WHERE name = 'Central Park';
-- Result:
-- name | geojson_geometry
-- Central Park | {"type":"Polygon","coordinates":[[[-73.97,40.76],...]]}Building a GeoJSON Feature
SQL (PostGIS)-- Build a complete GeoJSON Feature with properties
SELECT jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(geom)::jsonb,
'properties', jsonb_build_object(
'name', name,
'park_type', park_type,
'area_acres', area_acres
)
) AS feature
FROM parks
WHERE id = 1;Building a FeatureCollection
SQL (PostGIS)-- Build a complete GeoJSON FeatureCollection from a query
SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', jsonb_agg(
jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(geom)::jsonb,
'properties', jsonb_build_object(
'name', name,
'park_type', park_type,
'area_acres', area_acres
)
)
)
) AS geojson
FROM parks;This is the most common pattern: query your spatial table and return a complete FeatureCollection that can be sent directly to a web map client.
Controlling Coordinate Precision
SQL (PostGIS)-- Limit coordinates to 6 decimal places (~0.1m precision)
SELECT ST_AsGeoJSON(geom, 6) AS geojson_geometry FROM parks;
-- Limit to 4 decimal places (~11m precision) for smaller payloads
SELECT ST_AsGeoJSON(geom, 4) AS geojson_geometry FROM parks;Importing GeoJSON into PostGIS
ST_GeomFromGeoJSON — Parse GeoJSON Geometry
SQL (PostGIS)-- Insert a single feature from GeoJSON
INSERT INTO parks (name, park_type, geom)
VALUES (
'Prospect Park',
'city',
ST_SetSRID(
ST_GeomFromGeoJSON('{
"type": "Polygon",
"coordinates": [[
[-73.9712, 40.6602],
[-73.9625, 40.6553],
[-73.9580, 40.6620],
[-73.9685, 40.6710],
[-73.9712, 40.6602]
]]
}'),
4326
)
);Always wrap ST_GeomFromGeoJSON with ST_SetSRID(..., 4326) to ensure the geometry has the correct coordinate reference system.
Bulk Import from a GeoJSON File
SQL (PostGIS)-- Using ogr2ogr (part of GDAL) — the most reliable method
ogr2ogr -f "PostgreSQL" \
PG:"host=localhost dbname=mydb user=postgres" \
parks.geojson \
-nln parks \
-lco GEOMETRY_NAME=geom \
-lco FID=id
# Common ogr2ogr options:
# -nln Target table name
# -lco Layer creation options
# -append Append to existing table (instead of creating new)
# -overwrite Drop and recreate the table
# -t_srs Transform to target SRS (e.g., EPSG:4326)
# -sql Apply SQL filter to source dataImport from a FeatureCollection Using SQL
SQL (PostGIS)-- Parse a full FeatureCollection in SQL
-- Useful when receiving GeoJSON from an API
WITH features AS (
SELECT jsonb_array_elements(
'{"type":"FeatureCollection","features":[...]}'::jsonb -> 'features'
) AS feature
)
INSERT INTO parks (name, park_type, geom)
SELECT
feature -> 'properties' ->> 'name',
feature -> 'properties' ->> 'park_type',
ST_SetSRID(
ST_GeomFromGeoJSON(feature ->> 'geometry'),
4326
)
FROM features;Spatial Queries
The real power of PostGIS is spatial queries — finding features based on their geographic relationships. Here are the most common patterns, all returning GeoJSON.
Find Features Within a Bounding Box
SQL (PostGIS)-- Features within a bounding box (fast with GIST index)
SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', jsonb_agg(
jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(geom)::jsonb,
'properties', jsonb_build_object('name', name)
)
)
) AS geojson
FROM parks
WHERE geom && ST_MakeEnvelope(-74.05, 40.68, -73.90, 40.80, 4326);Find Features Near a Point
SQL (PostGIS)-- Parks within 2 km of a point, ordered by distance
SELECT name,
ST_Distance(geom::geography, ST_SetSRID(ST_MakePoint(-73.985, 40.748), 4326)::geography) AS distance_m,
ST_AsGeoJSON(geom) AS geometry
FROM parks
WHERE ST_DWithin(
geom::geography,
ST_SetSRID(ST_MakePoint(-73.985, 40.748), 4326)::geography,
2000 -- distance in meters
)
ORDER BY distance_m;Note: Cast to ::geography for distance calculations in meters. Without the cast, ST_Distance returns degrees.
Point in Polygon
SQL (PostGIS)-- Which neighborhood contains this point?
SELECT name, ST_AsGeoJSON(geom) AS geometry
FROM neighborhoods
WHERE ST_Contains(geom, ST_SetSRID(ST_MakePoint(-73.985, 40.748), 4326));Intersection and Clipping
SQL (PostGIS)-- Clip parks to a neighborhood boundary
SELECT
p.name,
ST_AsGeoJSON(ST_Intersection(p.geom, n.geom)) AS clipped_geometry
FROM parks p
JOIN neighborhoods n ON ST_Intersects(p.geom, n.geom)
WHERE n.name = 'Manhattan';Buffer Analysis
SQL (PostGIS)-- Create a 500m buffer around each park and return as GeoJSON
SELECT name,
ST_AsGeoJSON(
ST_Transform(
ST_Buffer(ST_Transform(geom, 3857), 500), -- buffer in meters
4326 -- back to WGS 84
)
) AS buffer_geometry
FROM parks;Aggregation — Dissolve / Union
SQL (PostGIS)-- Merge all parks of the same type into single geometries
SELECT park_type,
ST_AsGeoJSON(ST_Union(geom)) AS merged_geometry
FROM parks
GROUP BY park_type;Building a GeoJSON API
A common pattern is serving GeoJSON directly from PostGIS through a REST API. Here are examples in popular backend frameworks.
Node.js / Express
JavaScript (Node.js)import express from "express";
import pg from "pg";
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const app = express();
app.get("/api/parks", async (req, res) => {
const { rows } = await pool.query(`
SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', COALESCE(jsonb_agg(
jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(geom, 6)::jsonb,
'properties', jsonb_build_object(
'id', id, 'name', name, 'park_type', park_type
)
)
), '[]'::jsonb)
) AS geojson
FROM parks
`);
res.json(rows[0].geojson);
});Python / Flask
Python (Flask)from flask import Flask, jsonify
import psycopg2
import json
app = Flask(__name__)
@app.route("/api/parks")
def get_parks():
conn = psycopg2.connect(os.environ["DATABASE_URL"])
cur = conn.cursor()
cur.execute("""
SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', COALESCE(jsonb_agg(
jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(geom, 6)::jsonb,
'properties', jsonb_build_object(
'id', id, 'name', name, 'park_type', park_type
)
)
), '[]'::jsonb)
) AS geojson
FROM parks
""")
result = cur.fetchone()[0]
cur.close()
conn.close()
return jsonify(result)Bounding Box Filter from Query Parameters
SQL (PostGIS)-- API endpoint: /api/parks?bbox=-74.05,40.68,-73.90,40.80
app.get("/api/parks", async (req, res) => {
const { bbox } = req.query;
let query = `
SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', COALESCE(jsonb_agg(feature), '[]'::jsonb)
) AS geojson
FROM (
SELECT jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(geom, 6)::jsonb,
'properties', jsonb_build_object('id', id, 'name', name)
) AS feature
FROM parks
`;
const params = [];
if (bbox) {
const [west, south, east, north] = bbox.split(",").map(Number);
query += " WHERE geom && ST_MakeEnvelope($1, $2, $3, $4, 4326)";
params.push(west, south, east, north);
}
query += ") sub";
const { rows } = await pool.query(query, params);
res.json(rows[0].geojson);
});Geometry Validation
Invalid geometries (self-intersecting polygons, unclosed rings) can cause query errors. PostGIS provides tools to detect and fix them.
SQL (PostGIS)-- Check if geometries are valid
SELECT id, name, ST_IsValid(geom) AS is_valid,
ST_IsValidReason(geom) AS reason
FROM parks
WHERE NOT ST_IsValid(geom);
-- Auto-fix invalid geometries
UPDATE parks
SET geom = ST_MakeValid(geom)
WHERE NOT ST_IsValid(geom);
-- Validate before inserting GeoJSON
INSERT INTO parks (name, geom)
VALUES (
'New Park',
ST_MakeValid(
ST_SetSRID(ST_GeomFromGeoJSON('{"type":"Polygon","coordinates":[...]}'), 4326)
)
);You can also validate your GeoJSON before sending it to the database using our GeoJSON Validator.
Coordinate Systems and Reprojection
GeoJSON uses WGS 84 (EPSG:4326) with longitude/latitude coordinates. PostGIS can store and query in any coordinate system and reproject on the fly.
SQL (PostGIS)-- Reproject from Web Mercator (EPSG:3857) to WGS 84 for GeoJSON output
SELECT ST_AsGeoJSON(ST_Transform(geom, 4326)) AS geojson
FROM my_mercator_table;
-- Store in WGS 84 but compute area in a projected CRS for accuracy
SELECT name,
ST_Area(ST_Transform(geom, 3857)) AS area_sq_meters,
ST_AsGeoJSON(geom) AS geojson
FROM parks;
-- Or use the geography type for accurate measurements without reprojecting
SELECT name,
ST_Area(geom::geography) AS area_sq_meters
FROM parks;See Coordinate Reference Systems for more on how CRS works with GeoJSON.
Performance Tips
- Always create a GIST index on geometry columns:
CREATE INDEX ON table USING GIST (geom). Without it, every spatial query does a full table scan. - Use
&&for bounding box checks — the&&operator uses the spatial index and is much faster thanST_Intersectsalone. - Reduce coordinate precision in
ST_AsGeoJSON(geom, 6)to keep response payloads small. 6 decimal places gives ~0.1m accuracy. - Simplify geometries for web display:
ST_Simplify(geom, 0.001)for detailed views orST_Simplify(geom, 0.01)for overview maps. - Use
ST_SimplifyPreserveTopologyinstead ofST_Simplifyto prevent polygons from collapsing or self-intersecting. - Paginate large results — use
LIMITandOFFSETor cursor-based pagination rather than returning thousands of features. - Use
ST_DWithininstead ofST_Distancefor proximity filters —ST_DWithinuses the spatial index while aWHERE ST_Distance(...) < Xdoes not. - Consider
geographyvsgeometry— usegeographyfor distance/area calculations in meters without reprojection, but note that fewer functions support it and it can be slower for complex operations.
Quick Reference
| Task | Function |
|---|---|
| Geometry → GeoJSON | ST_AsGeoJSON(geom) |
| GeoJSON → Geometry | ST_GeomFromGeoJSON(json) |
| Set SRID | ST_SetSRID(geom, 4326) |
| Reproject | ST_Transform(geom, target_srid) |
| Bounding box filter | geom && ST_MakeEnvelope(w, s, e, n, 4326) |
| Distance filter | ST_DWithin(geom::geography, point::geography, meters) |
| Point in polygon | ST_Contains(polygon, point) |
| Intersection test | ST_Intersects(geom_a, geom_b) |
| Clip geometry | ST_Intersection(geom_a, geom_b) |
| Buffer | ST_Buffer(geom::geography, meters) |
| Merge / dissolve | ST_Union(geom) |
| Simplify | ST_Simplify(geom, tolerance) |
| Validate | ST_IsValid(geom) |
| Fix invalid | ST_MakeValid(geom) |
| Area (sq meters) | ST_Area(geom::geography) |
| Length (meters) | ST_Length(geom::geography) |
| Centroid | ST_Centroid(geom) |
Further Reading
- GeoJSON to WKT Converter — convert GeoJSON to Well-Known Text for direct SQL use
- GeoJSON Validator — validate your GeoJSON before importing
- Coordinate Reference Systems — CRS and projections explained
- GeoJSON Examples — sample data for all geometry types
- Web Mapping Developer Guide — display your PostGIS data on a web map