Shapefile round trips: what three pipelines changed
All three configured pipelines changed long field names, while their null and empty-text results differed. Mapshaper changed the two tested integers beyond JavaScript’s exact integer range; PyShp and GDAL/OGR returned those integers unchanged. Every pipeline retained both point coordinates in all twenty fixtures, but that coordinate result did not guarantee unchanged attributes.
Read the change, not a tool score
The table combines twenty unchanged synthetic fixtures across three explicit pipelines. PyShp received declared DBF schemas, while Mapshaper and ogr2ogr inferred their schemas from GeoJSON. That difference matters: PyShp’s six-decimal field definition rounded one fixture, while the other configured pipelines retained those input values. The counts describe these configurations, not a ranking of software quality.
“Exact properties” compares the returned keys and values with the original properties. JSON booleans remain distinct from numbers; numeric values compare numerically. “Exact coordinates” compares the two returned point positions. “What changed” names the observed difference for every No. The full JSON and CSV retain the inputs, outputs, field definitions and per-run evidence.
| Fixture | Pipeline/version | Exact properties | Exact coordinates | What changed |
|---|---|---|---|---|
| F01 | pyshp 3.1.6 | Yes | Yes | None in returned properties or coordinates. |
| F01 | mapshaper 0.7.97 | Yes | Yes | None in returned properties or coordinates. |
| F01 | gdal-ogr 3.11.5 | Yes | Yes | None in returned properties or coordinates. |
| F02 | pyshp 3.1.6 | No | Yes | Field population_total became population (10 characters). |
| F02 | mapshaper 0.7.97 | No | Yes | Field population_total became population (10 characters). |
| F02 | gdal-ogr 3.11.5 | No | Yes | Field population_total became population (10 characters). |
| F03 | pyshp 3.1.6 | No | Yes | Both names became population; dictionary keys collided. Both positional values survived. |
| F03 | mapshaper 0.7.97 | No | Yes | Fields became population and populati_1; both values survived. |
| F03 | gdal-ogr 3.11.5 | No | Yes | Fields became population and populati_1; both values survived. |
| F04 | pyshp 3.1.6 | Yes | Yes | None in returned properties or coordinates. |
| F04 | mapshaper 0.7.97 | Yes | Yes | None in returned properties or coordinates. |
| F04 | gdal-ogr 3.11.5 | Yes | Yes | None in returned properties or coordinates. |
| F05 | pyshp 3.1.6 | Yes | Yes | None in returned properties or coordinates. |
| F05 | mapshaper 0.7.97 | Yes | Yes | None in returned properties or coordinates. |
| F05 | gdal-ogr 3.11.5 | Yes | Yes | None in returned properties or coordinates. |
| F06 | pyshp 3.1.6 | Yes | Yes | None in returned properties or coordinates. |
| F06 | mapshaper 0.7.97 | Yes | Yes | None in returned properties or coordinates. |
| F06 | gdal-ogr 3.11.5 | Yes | Yes | None in returned properties or coordinates. |
| F07 | pyshp 3.1.6 | No | Yes | Null became the text None; empty text stayed empty. |
| F07 | mapshaper 0.7.97 | No | Yes | Null became empty text; null and empty text became indistinguishable. |
| F07 | gdal-ogr 3.11.5 | No | Yes | Empty text became null; the original null stayed null. |
| F08 | pyshp 3.1.6 | Yes | Yes | None in returned properties or coordinates. |
| F08 | mapshaper 0.7.97 | Yes | Yes | None in returned properties or coordinates. |
| F08 | gdal-ogr 3.11.5 | Yes | Yes | None in returned properties or coordinates. |
| F09 | pyshp 3.1.6 | Yes | Yes | None in returned properties or coordinates. |
| F09 | mapshaper 0.7.97 | Yes | Yes | None in returned properties or coordinates. |
| F09 | gdal-ogr 3.11.5 | Yes | Yes | None in returned properties or coordinates. |
| F10 | pyshp 3.1.6 | No | Yes | 1.23456789 became 1.234568; −0.0000004 became −0.0 under the six-decimal schema. |
| F10 | mapshaper 0.7.97 | Yes | Yes | None in returned properties or coordinates. |
| F10 | gdal-ogr 3.11.5 | Yes | Yes | None in returned properties or coordinates. |
| F11 | pyshp 3.1.6 | Yes | Yes | None in returned properties or coordinates. |
| F11 | mapshaper 0.7.97 | No | Yes | 9007199254740993→9007199254740992; 9007199254740995→9007199254740996. |
| F11 | gdal-ogr 3.11.5 | Yes | Yes | None in returned properties or coordinates. |
| F12 | pyshp 3.1.6 | Yes | Yes | None in returned properties or coordinates. |
| F12 | mapshaper 0.7.97 | Yes | Yes | None in returned properties or coordinates. |
| F12 | gdal-ogr 3.11.5 | Yes | Yes | None in returned properties or coordinates. |
| F13 | pyshp 3.1.6 | Yes | Yes | None in returned properties or coordinates. |
| F13 | mapshaper 0.7.97 | Yes | Yes | None in returned properties or coordinates. |
| F13 | gdal-ogr 3.11.5 | Yes | Yes | None in returned properties or coordinates. |
| F14 | pyshp 3.1.6 | Yes | Yes | None in returned properties or coordinates. |
| F14 | mapshaper 0.7.97 | Yes | Yes | None in returned properties or coordinates. |
| F14 | gdal-ogr 3.11.5 | Yes | Yes | None in returned properties or coordinates. |
| F15 | pyshp 3.1.6 | No | Yes | The 300-byte ASCII value was cut to 254 bytes. |
| F15 | mapshaper 0.7.97 | No | Yes | The 300-byte ASCII value was cut to 254 bytes. |
| F15 | gdal-ogr 3.11.5 | No | Yes | The 300-byte ASCII value was cut to 254 bytes. |
| F16 | pyshp 3.1.6 | No | Yes | Trailing spaces were removed; leading spaces survived. |
| F16 | mapshaper 0.7.97 | No | Yes | Leading and trailing spaces were removed. |
| F16 | gdal-ogr 3.11.5 | No | Yes | Leading and trailing spaces were removed. |
| F17 | pyshp 3.1.6 | No | Yes | Nested objects became Python-style text representations. |
| F17 | mapshaper 0.7.97 | No | Yes | Nested objects became null. |
| F17 | gdal-ogr 3.11.5 | Yes | Yes | None in returned properties or coordinates. |
| F18 | pyshp 3.1.6 | No | Yes | Arrays became Python-style text representations. |
| F18 | mapshaper 0.7.97 | No | Yes | Arrays became null. |
| F18 | gdal-ogr 3.11.5 | No | Yes | The tags array field was dropped; generated FID values 0 and 1 appeared. |
| F19 | pyshp 3.1.6 | Yes | Yes | None in returned properties or coordinates. |
| F19 | mapshaper 0.7.97 | Yes | Yes | None in returned properties or coordinates. |
| F19 | gdal-ogr 3.11.5 | Yes | Yes | None in returned properties or coordinates. |
| F20 | pyshp 3.1.6 | No | Yes | Numeric 42 became text 42; the existing text value survived. |
| F20 | mapshaper 0.7.97 | No | Yes | The text value became null; numeric 42 survived. |
| F20 | gdal-ogr 3.11.5 | No | Yes | Numeric 42 became text 42; the existing text value survived. |
Keep storage and dictionary projection separate
F03 exposes two different field-name outcomes. PyShp’s two long names both become population; its positional records still contain both values, but converting those records to a dictionary collapses the duplicate keys. That is a loss in the projected dictionary, not evidence that one value disappeared from the DBF file. Mapshaper and OGR instead return population and populati_1, retaining both values under changed names.
These are the kinds of changes to check before joining a returned table to another dataset. A new field name may be manageable if it is recorded deliberately. A silent collision is harder to spot because the dictionary still looks structurally valid. The original field definitions and PyShp positional records are retained so the different cases can be inspected1.
Nulls, numbers and text need separate checks
F07 starts with one null and one empty string. PyShp returns the null as the text None; Mapshaper returns both as empty strings; OGR returns both as null. Those are three distinct outcomes. A downstream expression that distinguishes “missing” from “present but blank” would receive different inputs even though the two points still occupy the same locations.
In F11, Mapshaper returns 9007199254740992 and 9007199254740996 for inputs 9007199254740993 and 9007199254740995. The other two pipelines retain the original integers. F15 is different: all three cut the 300-byte ASCII value to 254 bytes. Do not treat a numeric representation change and a width-limited text truncation as the same failure or assume that changing the geometry format alone fixes either2.
Examine nested and mixed values explicitly
The OGR round trip returns F17’s nested object unchanged, but its intermediate shapefile field is a String. That end-to-end result does not mean DBF acquired native nested-object storage. F18’s array field is unsupported in this configured OGR export: it disappears and generated FID values appear instead. The retained process warnings and returned schemas make those two cases distinguishable3.
Mixed values also depend on inference and declaration. For F20, PyShp and OGR return numeric 42 as text while preserving the existing text value. Mapshaper preserves 42 but returns the text value as null. If a field must retain one stable type, define and verify that requirement before selecting an export configuration.
Use the table viewer to inspect one fixture across all three implementations, then consult the method before reusing a result. The corpus has two synthetic points per fixture; it is deliberately small enough that each change can be explained directly.
Not covered on this page
- QGIS interface defaults or an export with different field options.
- Polygons, curves, multipart geometry, invalid geometry or coordinate transformation.
- Other encodings, larger values or every DBF implementation.
- Software rankings, performance measurements or a universal conformance score.
Sources
Code on this page is original and MIT licensed.
How we test: methodology. Found a mistake? Corrections.