typed-geojson
Strongly-typed GeoJSON for Rust.
Feature<G, P> / FeatureCollection<G, P>: generic over Geometry and
Properties, layered over the georust geojson
crate. With the specta feature it exports to TypeScript that is mutually
assignable with @types/geojson.
use ;
use ;
let raw = r#"{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-96.8, 32.8] },
"properties": { "id": 7, "name": "DFW", "temp_c": 31.5 }
}"#;
let feature: = from_str.unwrap;
assert_eq!;
What you get
Feature<G, P>/FeatureCollection<G, P>: native parameter order;Gdefaults to the [Geometry] union,Pto an untyped JSON object.- Typed geometry (
Point,LineString,Polygon,MultiPoint,MultiLineString,MultiPolygon,GeometryCollection, and theGeometryunion), each matching its native@types/geojsonshape. - serde that round-trips to/from spec GeoJSON: validates
"type", preserves foreign members (RFC 7946 §6.1), keepsid/bbox, omits an absentbbox. TryFrombridges to/from the untypedgeojsoncrate.- Optional
geo-typesfeature:From/TryFrombetween the geometry types andgeo-types, so a typed geometry flows into the georustgeoalgorithms layer.
Nullability lives in G
Geometry is required but may be null (RFC 7946); like native, that nullability
is a choice of G:
type Located = ; // geometry: Geometry (non-null)
type Unlocated = ; // geometry: Geometry | null
type PointFeat = ; // geometry: Point
Foreign members are preserved, not dropped
RFC 7946 §6.1 lets a GeoJSON object carry members the spec doesn't define — an
extension's "title", a producer's vendor field, a legacy "crs". The standard
geojson crate's typed path discards these; this crate keeps them on
Feature and FeatureCollection, so a parse → serialize round-trip is
byte-faithful and they also survive the geojson bridge.
use ;
let raw = r#"{"type":"Feature","geometry":null,"properties":null,"title":"Sensor 7"}"#;
let f: = from_str.unwrap;
assert_eq!;
assert_eq!; // round-trips byte-for-byte
Foreign members are runtime fidelity only — they are deliberately left out of
the specta/TypeScript export, because @types/geojson doesn't type them
either, and keeping them out of the static contract is what preserves the
assignability guarantee.
geo-types (feature)
Enable the geo-types feature to convert between the geometry types and
geo-types, matching upstream geojson's semantics: geo_types → ours is
infallible (From; a 3D geo_types never carries elevation, so positions are
2D), ours → geo_types is fallible (TryFrom; a position needs at least x
and y).
#
#
TypeScript (specta feature)
Feature<Point, Props> exports to a TS Feature<Point, Props> assignable to
and from GeoJSON.Feature<GeoJSON.Point, Props>: zero tsc errors, both ways.
let types = specta_types;
let ts = default
.export?;
Two details make it line up: bbox is a tuple union ([n,n,n,n] | [n,…×6]),
not number[]; each geometry's "type" is a string literal. ts/ holds the
assignability harness, gated in CI by tsc --noEmit.
Compatibility
CI gates the assignability promise across the versions consumers actually
resolve: @types/geojson from 7946.0.8 (the Feature/FeatureCollection
generics era) through latest, on the pinned TypeScript compiler, plus a
non-blocking canary against typescript@next.
Known caveat: under exactOptionalPropertyTypes, @types/geojson declares
optionals as bbox?: BBox | undefined while the generated bindings say
bbox?: Bbox, so tsc rejects assignment from the native types into the
bindings types. That rendering choice lives in the TypeScript exporter, not
in this crate, so the harness runs without the flag until the exporter grows
an option for it.
Benchmarks
cargo bench, a 1k-point FeatureCollection with typed properties, vs the
untyped geojson baseline:
| variant | deserialize | serialize |
|---|---|---|
untyped geojson |
~538 µs | ~176 µs |
| typed properties | ~341 µs | ~158 µs |
typed properties + our Geometry |
~396 µs | ~176 µs |
Typed is ~30% faster to deserialize (typed properties skip the untyped JSON
map); the untagged Geometry union adds ~16% on reads. Serialize is on par.
License
Licensed under either Apache-2.0 or MIT at your option. Contributions are dual-licensed as above unless stated otherwise.