Skip to main content

geofix

PyPI Python CI License: MIT

Fast, dependency-light GeoJSON validator and repairer for the command line, pre-commit hooks and CI. It checks files against RFC 7946 (structure, coordinates, winding, bounding boxes) and polygon topology (self-intersections, holes), tells you where each problem is, and can repair most of them, reporting every change it makes.

  • Pure Python, no required dependencies (only tomli on Python < 3.11 for reading pyproject.toml). Topology checks run without shapely or GEOS.
  • Streams large files: a FeatureCollection is read one feature at a time, and newline-delimited GeoJSON (GeoJSONSeq / RFC 8142, .geojsonl, .ndgeojson) one record at a time.
  • Made for CI: exit codes, --format github annotations, --format json, --select/--ignore rule codes, [tool.geofix] configuration, a pre-commit hook.
  • Explains CRS mistakes: "this looks like EPSG:3857 Web Mercator (the centre would be lon 28.99762, lat 40.97734)", UTM/TM eastings, swapped [lat, lon] axes.
  • Repairs safely: geofix fix never changes data without a recorded change, leaves valid files byte-for-byte untouched, and is idempotent.
pip install geofix            # pure Python
pip install "geofix[shapely]"  # + shapely.make_valid for complex topology repairs

Command line

geofix check

$ geofix check examples/bad.geojson
examples/bad.geojson:2:1: GF212 warning: exterior ring is clockwise [$.features[0].geometry.coordinates[0]]
    hint: Reverse the ring (ogr2ogr -lco RFC7946=YES writes the right order).
examples/bad.geojson:3:1: GF220 error: ring crosses itself at (29.025, 41.005) [$.features[1].geometry.coordinates[0]]
    hint: Split bow-ties into separate polygons or rebuild with shapely.make_valid.
examples/bad.geojson:4:1: GF109 warning: 1 consecutive duplicate position(s), first [29.05, 41] [$.features[2].geometry.coordinates[0][2]]
    hint: Remove the repeated positions.
examples/bad.geojson:4:1: GF210 error: ring is not closed: first position [29.04, 41], last [29.04, 41.01] [$.features[2].geometry.coordinates[0]]
    hint: Repeat the first position at the end of the ring.
examples/bad.geojson:4:1: GF108 info: 1 of 5 position(s) have more than 7 decimals [$.features[2].geometry.coordinates[0][3]]
    hint: Round coordinates; 6 decimals is ~10 cm, 7 is ~1 cm (RFC 7946 section 11.2).
examples/bad.geojson:5:1: GF006 warning: id 'parcel-3' is also used by $.features[2] [$.features[3].id]
    hint: Give every feature its own id, or drop ids that are not identifiers.
examples/bad.geojson:5:1: GF107 error: coordinates (x 3.228e+06..3.228e+06, y 5.009e+06..5.009e+06) look projected, not WGS 84 longitude/latitude: this looks like EPSG:3857 Web Mercator (the centre would be lon 28.99762, lat 40.97734) or a Gauss-Kruger/TM grid with zone-prefixed eastings [$.features[3].geometry.coordinates]
    hint: Reproject to EPSG:4326, e.g. ogr2ogr -t_srs EPSG:4326 -lco RFC7946=YES out.geojson in.geojson.
examples/bad.geojson:6:1: GF106 warning: latitude 129.0 is out of range while every longitude is within +/-90: positions look like [lat, lon] [$.features[4].geometry.coordinates[0]]
    hint: GeoJSON order is longitude first; swap x and y (EPSG:4326 axis order is a common cause).
examples/bad.geojson:7:1: GF200 warning: MultiPolygon has empty coordinates [$.features[5].geometry]
    hint: Remove the feature, or use "geometry": null for features without a location.
examples/bad.geojson:1:1: GF301 error: "bbox" [29, 41, 29.03, 41.01] does not contain the coordinates (extent [29, 41, 3228000.5, 5009000.25]) [$.bbox]
    hint: Recompute the bbox from the coordinates.

Found 10 issues (4 errors, 5 warnings, 1 info) in 1 of 1 file.
$ echo $?
1

Each line is file:line:column: CODE severity: message [JSON path]. The line is where the feature (or record) starts; the JSON path points inside it. Directories are searched recursively for .geojson, .geojsonl, .geojsons, .geojsonseq, .ndgeojson and .geo.json files; - reads standard input.

Useful options: --select GF2,GF3, --ignore GF007,GF108 (codes or prefixes, ALL), --fail-on error|warning|info, --precision N, --statistics, --no-hints, --format text|json|github.

geofix fix

$ geofix fix examples/bad.geojson -o fixed.geojson --no-hints
examples/bad.geojson: fixed 9 problems (GF108 x1, GF109 x1, GF200 x1, GF210 x1, GF212 x2, GF220 x2, GF301 x1)
examples/bad.geojson:5:1: GF006 warning: id 'parcel-3' is also used by $.features[2] [$.features[3].id]
examples/bad.geojson:5:1: GF107 error: coordinates (x 3.228e+06..3.228e+06, y 5.009e+06..5.009e+06) look projected, not WGS 84 longitude/latitude: this looks like EPSG:3857 Web Mercator (the centre would be lon 28.99762, lat 40.97734) or a Gauss-Kruger/TM grid with zone-prefixed eastings [$.features[3].geometry.coordinates]
examples/bad.geojson:6:1: GF106 warning: latitude 129.0 is out of range while every longitude is within +/-90: positions look like [lat, lon] [$.features[4].geometry.coordinates[0]]

Applied 9 fixes to 1 file. Found 3 remaining issues (1 error, 2 warnings) in 1 of 1 file.

What it did: rewound the clockwise shell, split the bow-tie into a two-part MultiPolygon, removed the duplicate vertex, closed the open ring, rounded the over-precise coordinate, removed the feature with an empty geometry and recomputed the collection bbox. It did not guess a CRS or swap axes: those are reported for a human to decide.

  • geofix fix FILE writes the result to standard output (the report goes to stderr); -o OUT writes a file; -i/--in-place rewrites inputs only when something changed; --dry-run reports what would change and exits 1 if anything would.
  • --show-changes lists every change instead of a per-file summary; --exit-nonzero-on-fix exits 1 when a file was modified; --no-shapely keeps repairs pure Python.
  • Collections are written one feature per line (readable diffs); --indent N pretty-prints single objects. Collection members keep their order, except bbox, which is written after features because it can only be computed once every feature has been repaired.
  • Repairs run in this order: round (GF108), drop duplicates (GF109), close rings (GF210), drop degenerate holes/parts (GF211/GF214), topology (GF220-GF222), rewind (GF212/GF213), recompute bboxes (GF300-GF302). A second geofix fix on the output changes nothing.
  • Rounding to the configured precision (7 decimals by default, ~1 cm) is a repair like any other: disable it with --ignore GF108 or change it with --precision 9.

geofix stats and geofix rules

$ geofix stats examples/bad.geojson
examples/bad.geojson
  kind:            FeatureCollection
  size:            1.2 kB
  features:        6
  geometry types:  Polygon 3, Point 1, LineString 1, MultiPolygon 1
  null / empty:    0 / 1
  positions:       18 (0 with z)
  max decimals:    10
  bbox:            [29, 41, 3.228e+06, 5.009e+06]
  ids:             6 (5 unique)
  properties:      name (5)
  read in:         0.00 s

geofix rules lists every rule (--format text|json|markdown).

Exit codes

Status Meaning
0 No issue at or above --fail-on (default error)
1 Issues at or above --fail-on; also fix --dry-run / --exit-nonzero-on-fix when a fix is (or was) needed
2 Usage or configuration error, or an input that cannot be read

Warnings do not fail a run by default; use --fail-on warning (or fail-on = "warning" in the configuration) for strict RFC 7946 conformance, e.g. ring winding.

Rules

Code Name Severity Checks geofix fix
GF000 invalid-json error The input is not valid UTF-8 JSON. report only
GF001 invalid-type error Missing or unknown "type" member. report only
GF002 missing-member error A required member is missing (Feature "geometry"/"properties", FeatureCollection "features", geometry "coordinates", GeometryCollection "geometries"). Adds "properties": {} or "geometry": null to features that lack them.
GF003 invalid-member error A member has the wrong JSON type (e.g. "features" is not an array, a feature is not a Feature object, "geometry" is not an object or null). report only
GF004 invalid-properties error "properties" must be a JSON object or null. report only
GF005 invalid-id error Feature "id" must be a string or a number. report only
GF006 duplicate-id warning Feature "id" is not unique within the collection. report only
GF007 foreign-member info Member that RFC 7946 does not define for this object (a foreign member). report only
GF008 reserved-member error Member that changes GeoJSON semantics on this object type (RFC 7946 section 7.1), e.g. "coordinates" on a Feature or "properties" on a geometry. report only
GF009 crs-member warning "crs" member present; RFC 7946 removed it and requires WGS 84 longitude/latitude. Removes "crs" when it already names WGS 84 (CRS84 / EPSG:4326).
GF010 nested-geometry-collection warning GeometryCollection nested inside another GeometryCollection (RFC 7946 3.1.8 says SHOULD avoid). report only
GF100 invalid-coordinates error "coordinates" has the wrong nesting depth or holds non-numeric values. report only
GF101 position-too-short error Position has fewer than two numbers. report only
GF102 position-too-long warning Position has more than three numbers (RFC 7946 3.1.1: SHOULD NOT). report only
GF103 non-finite-coordinate error Coordinate is NaN or Infinity (non-standard JSON literals, or overflowing numbers). report only
GF104 longitude-out-of-range error Longitude outside [-180, 180]. report only
GF105 latitude-out-of-range error Latitude outside [-90, 90]. report only
GF106 swapped-axes warning Coordinates look like [lat, lon] instead of [lon, lat]. report only
GF107 projected-coordinates error Coordinates look projected (metres: EPSG:3857, UTM, national TM grids), not WGS 84. report only
GF108 excessive-precision info Coordinates carry more decimals than the configured precision (default 7, ~1 cm). Rounds longitude/latitude to the configured precision.
GF109 duplicate-vertex warning Consecutive duplicate positions in a line or ring. Drops consecutive duplicate positions.
GF110 mixed-dimensions info Geometry mixes 2D and 3D positions. report only
GF200 empty-geometry warning Geometry has no coordinates (or a GeometryCollection has no geometries). Removes features whose geometry is empty from collections and sequences.
GF201 null-geometry info Feature has "geometry": null (allowed, but many tools skip such features). report only
GF202 line-too-short error LineString has fewer than two positions. report only
GF203 zero-length-line warning LineString whose positions are all identical. report only
GF210 unclosed-ring error Linear ring's first and last positions differ. Appends the first position to close the ring.
GF211 ring-too-short error Linear ring has fewer than four positions. Drops degenerate holes and polygon parts (always reported).
GF212 exterior-clockwise warning Exterior ring is clockwise; RFC 7946 3.1.6 requires counter-clockwise. Reverses the ring.
GF213 hole-counterclockwise warning Interior ring (hole) is counter-clockwise; RFC 7946 3.1.6 requires clockwise. Reverses the ring.
GF214 zero-area-ring error Linear ring encloses no area (all positions collinear or collapsed). Drops degenerate holes and polygon parts (always reported).
GF220 self-intersection error Ring crosses or touches itself. Splits single-crossing (bow-tie) rings in pure Python; other cases use shapely.make_valid when shapely is installed.
GF221 hole-outside-shell error Hole is not inside its exterior ring (lies outside it or crosses it). Rebuilt with shapely.make_valid when shapely is installed.
GF222 holes-overlap error Two holes of the same polygon cross, overlap or nest. Rebuilt with shapely.make_valid when shapely is installed.
GF230 antimeridian-crossing warning Consecutive positions jump more than 180 degrees of longitude; the segment probably crosses the antimeridian. report only
GF300 invalid-bbox error "bbox" is not an array of 2*n numbers with min <= max (west > east is allowed across the antimeridian). Recomputes the bbox from the coordinates.
GF301 bbox-mismatch error "bbox" does not contain all coordinates of the object. Recomputes the bbox from the coordinates.
GF302 bbox-not-tight info "bbox" is larger than the extent of the coordinates. Recomputes the bbox from the coordinates.

Notes on the checks:

  • Topology is planar, in the coordinate space of the file, like the OGC simple feature rules: within a ring any contact between non-adjacent edges is an error (crossing, touching, or a collinear spike); between rings, crossings and shared edges are errors, while a single touching point is allowed. Overlap between the parts of a MultiPolygon is not checked.
  • Antimeridian: RFC 7946 3.1.9 asks for geometries crossing 180 degrees to be cut into multi-part geometries. GF230 flags segments that jump more than 180 degrees of longitude; geofix does not cut them (use ogr2ogr -wrapdateline or the antimeridian package). A bbox with west > east is accepted as an antimeridian-crossing box (RFC 7946 5.2).
  • CRS heuristics (GF106/GF107) look at each geometry's extent. Values beyond +/-1000 are treated as metres; eastings of 100 000-900 000 m suggest UTM or a national TM grid (for example TUREF/TM), values within the Web Mercator square are back-projected so you can see where the data would land if it were EPSG:3857. [lat, lon] is only detectable when a latitude falls outside +/-90 while every longitude is within +/-90.
  • Precision (GF108) counts decimals of the shortest representation of each longitude and latitude (elevations are not checked).

Configuration

geofix reads [tool.geofix] from the nearest pyproject.toml (or --config FILE; --isolated ignores it). Command-line options override it.

[tool.geofix]
select = ["ALL"]            # codes or prefixes, e.g. ["GF0", "GF2"]
ignore = ["GF007"]          # foreign members are fine for us
fail-on = "warning"         # error | warning | info
precision = 6               # GF108 threshold and fix rounding
shapely = true              # let `geofix fix` use shapely.make_valid when installed
format = "text"             # text | json | github

pre-commit

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/YusufEminoglu/geofix_sdk
    rev: v0.1.0
    hooks:
      - id: geofix          # validate
      # - id: geofix-fix    # or repair in place (the commit stops if a file changed)

Both hooks run on .geojson, .geojsonl, .geojsons, .geojsonseq and .ndgeojson files and honour [tool.geofix]. Pass options with args, e.g. args: [--fail-on, warning].

GitHub Actions

# .github/workflows/geojson.yml
name: GeoJSON
on: [push, pull_request]
jobs:
  geofix:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: "3.13"
      - run: pip install geofix
      - run: geofix check --format github data/

--format github prints workflow commands so issues appear as annotations on the pull request diff, at the line where each feature starts:

::warning file=examples/bad.geojson,line=2,col=1,title=geofix GF212 (exterior-clockwise)::exterior ring is clockwise at $.features[0].geometry.coordinates[0]%0Ahint: Reverse the ring (ogr2ogr -lco RFC7946=YES writes the right order).
::error file=examples/bad.geojson,line=3,col=1,title=geofix GF220 (self-intersection)::ring crosses itself at (29.025, 41.005) at $.features[1].geometry.coordinates[0]%0Ahint: Split bow-ties into separate polygons or rebuild with shapely.make_valid.

Python API

import geofix

# Validate a parsed object, a JSON string/bytes, or a file (streamed).
issues = geofix.check({"type": "Point", "coordinates": [3228000.0, 5009000.0]})
issues = geofix.check_file("parcels.geojson", ignore=["GF007"], precision=6)
for issue in issues:
    print(issue.code, issue.severity, issue.line, issue.path, issue.message, issue.hint)

# Repair. The input is never modified; every modification is a Change.
result = geofix.fix(my_feature_collection)
result.data      # repaired object
result.changes   # [Change(code="GF212", path="$.features[0]...", message="rewound ...")]
result.issues    # what is still wrong after the repair

# Repair a file feature by feature (dst may equal src; None is a dry run; "-" is stdout).
result = geofix.fix_file("in.geojson", "out.geojson", use_shapely=False)

# Options can be shared.
opts = geofix.Options(select=["GF2", "GF3"], fail_on="warning", precision=7)
geofix.check_file("in.geojson", opts)

# Streaming building blocks.
with open("big.geojsonl", encoding="utf-8") as fh:
    for path, feature in geofix.iter_features(fh):
        ...

stats = geofix.stats_file("in.geojson")   # counts, bbox, property keys, max decimals
geofix.RULES["GF220"].summary             # rule registry

geofix.Validator and geofix.Repairer are the incremental engines behind these functions (feed them features one at a time); geofix.Reader produces the streaming events.

Performance

Pure Python 3.12 on an AMD Ryzen 4000-series laptop CPU (single thread, Windows), synthetic files (numbers vary with hardware; the test-suite only asserts generous limits):

File Size Positions check fix --dry-run
100 000 convex polygons (10 vertices) 37.6 MB 1.10 M 5.1 s 5.5 s
100 000 non-convex polygons, 10% with holes 44.1 MB 1.37 M 7.9 s 8.2 s
one ring, 1 000 000 vertices (ellipse) 25.8 MB 1.00 M 4.6 s 4.6 s
one ring, 1 000 000 vertices (wavy, 1 000 lobes) 25.8 MB 1.00 M 8.1 s 7.9 s

Repairing every feature of the 100 000-polygon file (all clockwise and over-precise: 200 000 changes) and writing the result takes about 21 s; a second run finds nothing to do in 5.4 s.

How: positions are validated with C-level map/itemgetter passes; the precision test is a regex over the repr of a whole coordinate list (exact round() only runs when it matches); convex rings skip the segment search; larger rings are split into x/y-monotone chains (whose own segments cannot meet) and only chains with overlapping boxes are bisected against each other, as in GEOS, with a uniform grid for zig-zag data. Memory is bounded by the largest single feature, not the file.

How geofix compares

geofix @mapbox/geojsonhint geojson (Python) shapely is_valid / make_valid ogr2ogr -lco RFC7946=YES
Runtime Python, no required deps Node.js Python Python + GEOS (bundled in wheels) GDAL
RFC 7946 structure checks yes yes partial (is_valid / errors()) no (geometry only) no (it converts)
Winding order (right-hand rule) check + fix check no no (orient can fix) writes CCW/CW
Self-intersections, holes check; fix bow-ties, rest via shapely no no yes (GEOS) no
bbox, ids, foreign members, crs yes partly no no writes a new file
Projected-CRS / swapped-axes hints yes no no no n/a (reprojects when told the CRS)
Reports what was changed yes (per change) n/a n/a no no
Streams large files / GeoJSONSeq yes no no n/a yes
CI output (exit codes, GitHub annotations) yes CLI only no no no

Use shapely/GEOS when you need full OGC validity semantics on arbitrary geometries (geofix calls shapely.make_valid for cases it cannot repair itself), and GDAL when you need to reproject or convert formats; geofix is the lightweight gate that tells you which of those you need, in a form CI understands.

Development

python -m pip install -e ".[dev,shapely]"
python -m pytest
python -m ruff check . && python -m ruff format --check src tests
python -m mypy src

License

MIT, see LICENSE. Citation metadata: CITATION.cff.

Metadata

Release files for geofix 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for geofix 0.1.1
File Size Uploaded Provenance Actions
geofix-0.1.1.tar.gz 79.0 kB With provenance Details for geofix-0.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for geofix 0.1.1
File Interpreter ABI Platform Actions
geofix-0.1.1-py3-none-any.whl Python 3 none any Details for geofix-0.1.1-py3-none-any.whl

Total release size: 137.1 kB

Release files / geofix-0.1.1.tar.gz

Download URL geofix-0.1.1.tar.gz
Size 79.0 kB
Tags Source
SHA-256 checksum
How to use checksums
c31499b73a088698c22951ba555a5a36764f2d9a81df9494619295fbb5e71d2f
BLAKE2b-256 checksum
How to use checksums
7568092b4432721454171a999e6d9d94f3b88d7dde261e88176aafa4bd587400
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

Release files / geofix-0.1.1-py3-none-any.whl

Download URL geofix-0.1.1-py3-none-any.whl
Size 58.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
62d49da8645667f02d3edc11f0f65f3a21c279540334d26d477d2c0a74121db1
BLAKE2b-256 checksum
How to use checksums
68a7aa1dcaafc6fbc20ec843f8d02ea5b6c1c5e9a9964d686b12047e6b706f57
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

Security for geofix 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for geofix-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page