Skip to main content

typed_geojson/
lib.rs

1//! Strongly-typed [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) for Rust.
2//!
3//! The [`geojson`] crate models a Feature's `properties` as an untyped
4//! `Option<serde_json::Map<String, Value>>`. This crate adds generics,
5//! [`Feature<G, P>`] and [`FeatureCollection<G, P>`], typed over both the
6//! `G`eometry and the `P`roperties, in the same parameter order as
7//! `@types/geojson`'s `Feature<G, P>` so the two interoperate.
8//!
9//! `G` defaults to the typed [`Geometry`] union (which mirrors
10//! `@types/geojson`'s `Geometry` and exports to TypeScript via the `specta`
11//! feature) and `P` to [`Properties`] (an untyped JSON object, like native
12//! `GeoJsonProperties`); pick your own `P` for typed properties:
13//!
14//! Per RFC 7946 a Feature's geometry is mandatory but may be `null`; like
15//! native, that nullability lives in `G`: `Feature<Geometry>` is non-null,
16//! `Feature<Option<Geometry>>` (→ TS `Geometry | null`) is the nullable form.
17//! [`geojson::Geometry`] interop is available through the [`Feature`] /
18//! [`Geometry`] `TryFrom` bridges.
19//!
20//! ```
21//! use serde::{Deserialize, Serialize};
22//! use typed_geojson::{Feature, Geometry};
23//!
24//! #[derive(Serialize, Deserialize, Debug, PartialEq)]
25//! struct Station {
26//!     id: u32,
27//!     name: String,
28//!     temp_c: f64,
29//! }
30//!
31//! let raw = r#"{
32//!     "type": "Feature",
33//!     "geometry": { "type": "Point", "coordinates": [-96.8, 32.8] },
34//!     "properties": { "id": 7, "name": "DFW", "temp_c": 31.5 }
35//! }"#;
36//!
37//! let feature: Feature<Geometry, Station> = serde_json::from_str(raw).unwrap();
38//! assert_eq!(feature.properties.name, "DFW");
39//!
40//! // round-trips back to standard GeoJSON
41//! let back: Feature<Geometry, Station> =
42//!     serde_json::from_str(&serde_json::to_string(&feature).unwrap()).unwrap();
43//! assert_eq!(back, feature);
44//! ```
45//!
46//! It also bridges to the untyped [`geojson`] crate (whose geometry is nullable,
47//! so use `Option<Geometry>`):
48//!
49//! ```
50//! use typed_geojson::{Feature, Geometry, Point};
51//!
52//! // geojson stores properties as a JSON object, so `P` must serialize to one.
53//! let f: Feature<Option<Geometry>, serde_json::Value> = Feature::new(
54//!     Some(Geometry::Point(Point::new(vec![-96.8, 32.8]))),
55//!     serde_json::json!({ "name": "DFW" }),
56//! );
57//! let untyped: geojson::Feature = f.try_into().unwrap();
58//! assert!(untyped.geometry.is_some());
59//! ```
60//!
61//! Unknown top-level members (RFC 7946 §6.1 *foreign members* — an extension's
62//! `"title"`, a vendor field) are preserved rather than dropped; see
63//! [`ForeignMembers`]. With the `geo-types` feature, the geometry types convert
64//! to and from [`geo_types`] so a typed geometry flows into georust's `geo`
65//! algorithms:
66//!
67//! ```
68//! # #[cfg(feature = "geo-types")]
69//! # {
70//! use typed_geojson::Point;
71//!
72//! let p: geo_types::Point<f64> = Point::new(vec![-96.8, 32.8]).try_into().unwrap();
73//! assert_eq!(p, geo_types::Point::new(-96.8, 32.8));
74//! # }
75//! ```
76
77use std::fmt;
78use std::marker::PhantomData;
79
80use serde::de::{self, Deserializer, MapAccess, Visitor};
81use serde::ser::{SerializeMap, Serializer};
82use serde::{Deserialize, Serialize};
83
84mod geometry;
85pub use geometry::*;
86
87// `From`/`TryFrom` between our geometry types and `geo_types` (impls only, so a
88// private module suffices — trait impls apply crate-wide regardless).
89#[cfg(feature = "geo-types")]
90mod geo_types_bridge;
91
92/// `specta` export-only marker: a JSON number that maps to the native TS
93/// `number`.
94///
95/// specta-typescript renders Rust `f64` as `number | null` (to model `NaN` /
96/// `Infinity`, which serde_json writes as `null`). GeoJSON coordinates and
97/// bboxes are finite reals, so for the TS bindings we override those numeric
98/// fields to this marker (which renders as a plain `number`) via
99/// `#[specta(type = …)]`. serde and the Rust API keep the real `f64`.
100#[cfg(feature = "specta")]
101pub(crate) struct TsNumber;
102
103#[cfg(feature = "specta")]
104impl specta::Type for TsNumber {
105    fn definition(_: &mut specta::Types) -> specta::datatype::DataType {
106        specta::datatype::DataType::Primitive(specta::datatype::Primitive::i32)
107    }
108}
109
110/// The default, untyped `properties` of a [`Feature`]: a JSON object or `null`
111/// (RFC 7946 §3.2). Mirrors `@types/geojson`'s
112/// `GeoJsonProperties = { [name: string]: any } | null`.
113pub type Properties = Option<serde_json::Map<String, serde_json::Value>>;
114
115/// A GeoJSON object's *foreign members* (RFC 7946 §6.1): members that the
116/// specification does not define — an extension's `"title"`, a vendor field, a
117/// legacy `"crs"`, and so on. The standard [`geojson`] crate documents that its
118/// typed path **drops these on the floor**; this crate keeps them, so a
119/// parse → serialize round-trip is byte-faithful and nothing a producer wrote
120/// is silently lost.
121///
122/// They are stored as a JSON object and serialized **flattened** at the top
123/// level of the enclosing object — never nested — exactly as they arrive on the
124/// wire. An empty map serializes to nothing (no key is emitted), so a Feature
125/// with no foreign members is byte-identical to one built without this field.
126///
127/// Foreign members are runtime fidelity only: they are deliberately **left out
128/// of the `specta` / TypeScript export**. `@types/geojson` does not model them
129/// either — they are excess properties there — so keeping them out of the
130/// static contract is exactly what preserves the assignability guarantee.
131///
132/// ```
133/// use typed_geojson::{Feature, Geometry};
134///
135/// // A Feature carrying an extension member alongside the spec fields.
136/// let raw = r#"{"type":"Feature","geometry":null,"properties":null,"title":"Sensor 7"}"#;
137/// let f: Feature<Option<Geometry>> = serde_json::from_str(raw).unwrap();
138/// assert_eq!(f.foreign_members["title"], serde_json::json!("Sensor 7"));
139///
140/// // …and it survives serialization untouched (here, byte-for-byte).
141/// assert_eq!(serde_json::to_string(&f).unwrap(), raw);
142/// ```
143pub type ForeignMembers = serde_json::Map<String, serde_json::Value>;
144
145/// A GeoJSON bounding box (RFC 7946 §5): a flat array of `2*n` numbers, either
146/// 4 (2D, `[west, south, east, north]`) or 6 (3D, with min/max elevation).
147///
148/// Modeled as a tuple union to match `@types/geojson`'s
149/// `BBox = [number, number, number, number] | [number, …×6]`, so it is
150/// assignable **to and from** the native type (a plain `number[]` is not).
151///
152/// Deserializes from a flat array of 4 or 6 numbers; any other length errors.
153#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
154#[cfg_attr(feature = "specta", derive(specta::Type))]
155#[serde(untagged)]
156pub enum Bbox {
157    /// 2D bounding box: `[west, south, east, north]`.
158    // specta `type =` can't parse an array type (`[T; N]`), but a tuple renders
159    // as the same TS tuple, and matches native `BBox`'s `[number, number, …]`.
160    D2(
161        #[cfg_attr(feature = "specta", specta(type = (TsNumber, TsNumber, TsNumber, TsNumber)))]
162        [f64; 4],
163    ),
164    /// 3D bounding box: `[west, south, min-elev, east, north, max-elev]`.
165    D3(
166        #[cfg_attr(feature = "specta", specta(type = (TsNumber, TsNumber, TsNumber, TsNumber, TsNumber, TsNumber)))]
167         [f64; 6],
168    ),
169}
170
171impl From<Bbox> for Vec<f64> {
172    fn from(bbox: Bbox) -> Self {
173        match bbox {
174            Bbox::D2(a) => a.to_vec(),
175            Bbox::D3(a) => a.to_vec(),
176        }
177    }
178}
179
180/// Build a [`Bbox`] from a flat `Vec<f64>` (as the untyped [`geojson`] crate
181/// stores it), accepting only the RFC 7946 lengths of 4 or 6.
182pub(crate) fn bbox_from_vec(v: Vec<f64>) -> Result<Bbox, serde_json::Error> {
183    match v.len() {
184        4 => Ok(Bbox::D2([v[0], v[1], v[2], v[3]])),
185        6 => Ok(Bbox::D3([v[0], v[1], v[2], v[3], v[4], v[5]])),
186        n => Err(<serde_json::Error as de::Error>::custom(format!(
187            "bbox must have 4 or 6 numbers, found {n}"
188        ))),
189    }
190}
191
192/// A GeoJSON Feature `id` — a string or a number (RFC 7946 §3.2).
193#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
194#[cfg_attr(feature = "specta", derive(specta::Type))]
195#[serde(untagged)]
196pub enum Id {
197    String(String),
198    // serde keeps full numeric fidelity; specta exports it as the native TS
199    // `number` (via [`TsNumber`] — `f64` would render as `number | null`).
200    Number(#[cfg_attr(feature = "specta", specta(type = TsNumber))] serde_json::Number),
201}
202
203/// A GeoJSON `Feature` with typed `G`eometry and `P`roperties.
204///
205/// `G` defaults to the [`Geometry`] union and `P` to [`Properties`], matching
206/// `@types/geojson`'s `Feature<G = Geometry, P = GeoJsonProperties>`. Pin `G` to
207/// a single geometry (e.g. [`Point`]) and/or set `P` to your own type.
208///
209/// Geometry is required but may be `null` (RFC 7946 §3.2); that nullability
210/// lives in `G` — `Feature<Option<Geometry>, P>` is the nullable form.
211///
212/// ```
213/// use typed_geojson::{Feature, Point};
214///
215/// let f: Feature<Point, &str> = Feature::new(Point::new(vec![-96.8, 32.8]), "DFW");
216///
217/// assert_eq!(
218///     serde_json::to_string(&f).unwrap(),
219///     r#"{"type":"Feature","geometry":{"type":"Point","coordinates":[-96.8,32.8]},"properties":"DFW"}"#,
220/// );
221/// ```
222#[derive(Clone, Debug, PartialEq)]
223pub struct Feature<G = Geometry, P = Properties> {
224    pub geometry: G,
225    pub properties: P,
226    pub id: Option<Id>,
227    pub bbox: Option<Bbox>,
228    /// Foreign members (RFC 7946 §6.1) preserved verbatim through serde and the
229    /// [`geojson`] bridge; empty when there are none. See [`ForeignMembers`].
230    pub foreign_members: ForeignMembers,
231}
232
233impl<G, P> Feature<G, P> {
234    /// A `Feature` with just a geometry and properties (no `id`/`bbox`, no
235    /// foreign members).
236    ///
237    /// Nullability lives in `G`: use `Feature::<Geometry, _>::new(geom, …)` for
238    /// a required geometry, or `Feature::<Option<Geometry>, _>::new(None, …)`
239    /// for the RFC 7946 "unlocated" (null-geometry) case.
240    pub fn new(geometry: G, properties: P) -> Self {
241        Self {
242            geometry,
243            properties,
244            id: None,
245            bbox: None,
246            foreign_members: ForeignMembers::new(),
247        }
248    }
249}
250
251impl<G: Serialize, P: Serialize> Serialize for Feature<G, P> {
252    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
253        let mut len = 3; // type, geometry, properties are always present
254        if self.id.is_some() {
255            len += 1;
256        }
257        if self.bbox.is_some() {
258            len += 1;
259        }
260        len += self.foreign_members.len();
261        let mut map = serializer.serialize_map(Some(len))?;
262        map.serialize_entry("type", "Feature")?;
263        // RFC 7946: `geometry` is mandatory but may be null.
264        map.serialize_entry("geometry", &self.geometry)?;
265        map.serialize_entry("properties", &self.properties)?;
266        if let Some(id) = &self.id {
267            map.serialize_entry("id", id)?;
268        }
269        if let Some(bbox) = &self.bbox {
270            map.serialize_entry("bbox", bbox)?;
271        }
272        // RFC 7946 §6.1: foreign members sit at the top level of the object.
273        for (name, value) in &self.foreign_members {
274            map.serialize_entry(name, value)?;
275        }
276        map.end()
277    }
278}
279
280// A Feature's member names. Known members map to their variant; anything else
281// is an RFC 7946 §6.1 foreign member and keeps its name (owned only for those
282// unknown keys — known keys never allocate).
283enum FeatureKey {
284    Type,
285    Geometry,
286    Properties,
287    Id,
288    Bbox,
289    Foreign(String),
290}
291
292impl<'de> Deserialize<'de> for FeatureKey {
293    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
294        struct KeyVisitor;
295        impl<'de> Visitor<'de> for KeyVisitor {
296            type Value = FeatureKey;
297
298            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
299                f.write_str("a GeoJSON Feature member name")
300            }
301
302            fn visit_str<E: de::Error>(self, v: &str) -> Result<FeatureKey, E> {
303                Ok(match v {
304                    "type" => FeatureKey::Type,
305                    "geometry" => FeatureKey::Geometry,
306                    "properties" => FeatureKey::Properties,
307                    "id" => FeatureKey::Id,
308                    "bbox" => FeatureKey::Bbox,
309                    _ => FeatureKey::Foreign(v.to_owned()),
310                })
311            }
312        }
313        deserializer.deserialize_str(KeyVisitor)
314    }
315}
316
317impl<'de, G: Deserialize<'de>, P: Deserialize<'de>> Deserialize<'de> for Feature<G, P> {
318    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
319        struct FeatureVisitor<G, P>(PhantomData<(G, P)>);
320
321        impl<'de, G: Deserialize<'de>, P: Deserialize<'de>> Visitor<'de> for FeatureVisitor<G, P> {
322            type Value = Feature<G, P>;
323
324            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
325                f.write_str("a GeoJSON Feature object")
326            }
327
328            fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Feature<G, P>, M::Error> {
329                let mut had_type = false;
330                let mut geometry: Option<G> = None;
331                let mut properties: Option<P> = None;
332                let mut id: Option<Id> = None;
333                let mut bbox: Option<Bbox> = None;
334                let mut foreign_members = ForeignMembers::new();
335
336                while let Some(key) = map.next_key()? {
337                    match key {
338                        FeatureKey::Type => {
339                            let ty: String = map.next_value()?;
340                            if ty != "Feature" {
341                                return Err(de::Error::custom(format!(
342                                    "expected `type` to be \"Feature\", found {ty:?}"
343                                )));
344                            }
345                            had_type = true;
346                        }
347                        FeatureKey::Geometry => geometry = Some(map.next_value()?),
348                        FeatureKey::Properties => properties = Some(map.next_value()?),
349                        FeatureKey::Id => id = map.next_value()?,
350                        FeatureKey::Bbox => bbox = map.next_value()?,
351                        // Preserve unknown keys as RFC 7946 §6.1 foreign members.
352                        FeatureKey::Foreign(name) => {
353                            foreign_members.insert(name, map.next_value()?);
354                        }
355                    }
356                }
357
358                if !had_type {
359                    return Err(de::Error::missing_field("type"));
360                }
361                // RFC 7946: the `geometry` member is mandatory (its value may
362                // be null, which a nullable `G` such as `Option<_>` accepts).
363                Ok(Feature {
364                    geometry: geometry.ok_or_else(|| de::Error::missing_field("geometry"))?,
365                    properties: properties.ok_or_else(|| de::Error::missing_field("properties"))?,
366                    id,
367                    bbox,
368                    foreign_members,
369                })
370            }
371        }
372
373        deserializer.deserialize_map(FeatureVisitor(PhantomData))
374    }
375}
376
377/// A GeoJSON `FeatureCollection` of typed features.
378///
379/// `collect` from any iterator of [`Feature`]s:
380///
381/// ```
382/// use typed_geojson::{Feature, FeatureCollection, Point};
383///
384/// let fc: FeatureCollection<Point, &str> = [
385///     Feature::new(Point::new(vec![0.0, 0.0]), "origin"),
386///     Feature::new(Point::new(vec![1.0, 1.0]), "ne"),
387/// ]
388/// .into_iter()
389/// .collect();
390///
391/// assert_eq!(fc.features.len(), 2);
392/// assert!(serde_json::to_string(&fc).unwrap().starts_with(r#"{"type":"FeatureCollection""#));
393/// ```
394#[derive(Clone, Debug, PartialEq)]
395pub struct FeatureCollection<G = Geometry, P = Properties> {
396    pub features: Vec<Feature<G, P>>,
397    pub bbox: Option<Bbox>,
398    /// Foreign members (RFC 7946 §6.1) preserved verbatim; empty when there are
399    /// none. See [`ForeignMembers`].
400    pub foreign_members: ForeignMembers,
401}
402
403impl<G, P> FromIterator<Feature<G, P>> for FeatureCollection<G, P> {
404    fn from_iter<I: IntoIterator<Item = Feature<G, P>>>(iter: I) -> Self {
405        Self {
406            features: iter.into_iter().collect(),
407            bbox: None,
408            foreign_members: ForeignMembers::new(),
409        }
410    }
411}
412
413impl<G: Serialize, P: Serialize> Serialize for FeatureCollection<G, P> {
414    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
415        let mut len = 2; // type, features
416        if self.bbox.is_some() {
417            len += 1;
418        }
419        len += self.foreign_members.len();
420        let mut map = serializer.serialize_map(Some(len))?;
421        map.serialize_entry("type", "FeatureCollection")?;
422        map.serialize_entry("features", &self.features)?;
423        if let Some(bbox) = &self.bbox {
424            map.serialize_entry("bbox", bbox)?;
425        }
426        // RFC 7946 §6.1: foreign members sit at the top level of the object.
427        for (name, value) in &self.foreign_members {
428            map.serialize_entry(name, value)?;
429        }
430        map.end()
431    }
432}
433
434// A FeatureCollection's member names; see [`FeatureKey`].
435enum CollectionKey {
436    Type,
437    Features,
438    Bbox,
439    Foreign(String),
440}
441
442impl<'de> Deserialize<'de> for CollectionKey {
443    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
444        struct KeyVisitor;
445        impl<'de> Visitor<'de> for KeyVisitor {
446            type Value = CollectionKey;
447
448            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
449                f.write_str("a GeoJSON FeatureCollection member name")
450            }
451
452            fn visit_str<E: de::Error>(self, v: &str) -> Result<CollectionKey, E> {
453                Ok(match v {
454                    "type" => CollectionKey::Type,
455                    "features" => CollectionKey::Features,
456                    "bbox" => CollectionKey::Bbox,
457                    _ => CollectionKey::Foreign(v.to_owned()),
458                })
459            }
460        }
461        deserializer.deserialize_str(KeyVisitor)
462    }
463}
464
465impl<'de, G: Deserialize<'de>, P: Deserialize<'de>> Deserialize<'de> for FeatureCollection<G, P> {
466    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
467        struct CollectionVisitor<G, P>(PhantomData<(G, P)>);
468
469        impl<'de, G: Deserialize<'de>, P: Deserialize<'de>> Visitor<'de> for CollectionVisitor<G, P> {
470            type Value = FeatureCollection<G, P>;
471
472            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
473                f.write_str("a GeoJSON FeatureCollection object")
474            }
475
476            fn visit_map<M: MapAccess<'de>>(
477                self,
478                mut map: M,
479            ) -> Result<FeatureCollection<G, P>, M::Error> {
480                let mut had_type = false;
481                let mut features: Option<Vec<Feature<G, P>>> = None;
482                let mut bbox: Option<Bbox> = None;
483                let mut foreign_members = ForeignMembers::new();
484
485                while let Some(key) = map.next_key()? {
486                    match key {
487                        CollectionKey::Type => {
488                            let ty: String = map.next_value()?;
489                            if ty != "FeatureCollection" {
490                                return Err(de::Error::custom(format!(
491                                    "expected `type` to be \"FeatureCollection\", found {ty:?}"
492                                )));
493                            }
494                            had_type = true;
495                        }
496                        CollectionKey::Features => features = Some(map.next_value()?),
497                        CollectionKey::Bbox => bbox = map.next_value()?,
498                        // Preserve unknown keys as RFC 7946 §6.1 foreign members.
499                        CollectionKey::Foreign(name) => {
500                            foreign_members.insert(name, map.next_value()?);
501                        }
502                    }
503                }
504
505                if !had_type {
506                    return Err(de::Error::missing_field("type"));
507                }
508                Ok(FeatureCollection {
509                    features: features.ok_or_else(|| de::Error::missing_field("features"))?,
510                    bbox,
511                    foreign_members,
512                })
513            }
514        }
515
516        deserializer.deserialize_map(CollectionVisitor(PhantomData))
517    }
518}
519
520// --- bridges to/from the untyped `geojson` crate (default geometry only) ------
521
522impl From<Id> for geojson::feature::Id {
523    fn from(id: Id) -> Self {
524        match id {
525            Id::String(s) => geojson::feature::Id::String(s),
526            Id::Number(n) => geojson::feature::Id::Number(n),
527        }
528    }
529}
530
531impl From<geojson::feature::Id> for Id {
532    fn from(id: geojson::feature::Id) -> Self {
533        match id {
534            geojson::feature::Id::String(s) => Id::String(s),
535            geojson::feature::Id::Number(n) => Id::Number(n),
536        }
537    }
538}
539
540// Bridges to/from the untyped `geojson::Feature`, whose geometry is nullable,
541// so the typed side is `Feature<Option<Geometry>, P>`. Geometry crosses via the
542// `Geometry` <-> `geojson::Geometry` `TryFrom`s (see `geometry.rs`).
543impl<P: Serialize> TryFrom<Feature<Option<Geometry>, P>> for geojson::Feature {
544    type Error = serde_json::Error;
545
546    fn try_from(f: Feature<Option<Geometry>, P>) -> Result<Self, Self::Error> {
547        let properties = match serde_json::to_value(&f.properties)? {
548            serde_json::Value::Object(map) => Some(map),
549            serde_json::Value::Null => None,
550            _ => {
551                return Err(<serde_json::Error as serde::ser::Error>::custom(
552                    "Feature properties must serialize to a JSON object or null",
553                ));
554            }
555        };
556        Ok(geojson::Feature {
557            bbox: f.bbox.map(Into::into),
558            geometry: f.geometry.map(geojson::Geometry::try_from).transpose()?,
559            id: f.id.map(Into::into),
560            properties,
561            // Carry foreign members across (RFC 7946 §6.1); `geojson` normalizes
562            // an empty map to `None`, so match that rather than emit `Some({})`.
563            foreign_members: (!f.foreign_members.is_empty()).then_some(f.foreign_members),
564        })
565    }
566}
567
568impl<P: serde::de::DeserializeOwned> TryFrom<geojson::Feature> for Feature<Option<Geometry>, P> {
569    type Error = serde_json::Error;
570
571    fn try_from(f: geojson::Feature) -> Result<Self, Self::Error> {
572        let value = match f.properties {
573            Some(map) => serde_json::Value::Object(map),
574            None => serde_json::Value::Null,
575        };
576        Ok(Feature {
577            geometry: f.geometry.map(Geometry::try_from).transpose()?,
578            properties: serde_json::from_value(value)?,
579            id: f.id.map(Into::into),
580            bbox: f.bbox.map(bbox_from_vec).transpose()?,
581            // Preserve any foreign members the untyped Feature carried.
582            foreign_members: f.foreign_members.unwrap_or_default(),
583        })
584    }
585}
586
587/// Register every public typed-geojson type into a [`specta::Types`] collection,
588/// ready to hand to a language exporter (e.g. `specta_typescript`).
589///
590/// The collection contains the generic `Feature<G, P>` / `FeatureCollection<G, P>`
591/// plus `Geometry` (and each named geometry), `Id`, and `Bbox` — shaped to be
592/// mutually assignable with `@types/geojson`. Requires the `specta` feature.
593///
594/// ```
595/// # #[cfg(feature = "specta")]
596/// # {
597/// let types = typed_geojson::specta_types();
598/// // let ts = specta_typescript::Typescript::default()
599/// //     .export(&types, specta_serde::Format)?;
600/// # }
601/// ```
602#[cfg(feature = "specta")]
603pub fn specta_types() -> specta::Types {
604    // `Feature`/`FeatureCollection` are generic; the concrete `Geometry` args
605    // here only satisfy registration — specta emits the generic `<G, P>` form.
606    specta::Types::default()
607        .register::<__ts::Feature<Geometry, Geometry>>()
608        .register::<__ts::FeatureCollection<Geometry, Geometry>>()
609        .register::<Geometry>()
610        .register::<Id>()
611        .register::<Bbox>()
612}
613
614// --- specta export shadows ----------------------------------------------------
615//
616// `Feature` / `FeatureCollection` use *manual* serde and so have no `type`
617// field for `#[derive(specta::Type)]` to see. These shadows mirror the exact
618// wire shape — including the literal `"type"` tag — and exist only to drive
619// TypeScript generation. They are not part of the public API.
620//
621// The runtime `foreign_members` map (RFC 7946 §6.1) is deliberately absent
622// here: `@types/geojson` does not type foreign members either, so surfacing
623// one would break the assignability promise. They stay runtime-only fidelity.
624#[cfg(feature = "specta")]
625#[doc(hidden)]
626#[allow(dead_code)]
627pub mod __ts {
628    use serde::{Deserialize, Serialize};
629
630    use super::{Bbox, Id};
631
632    // These derive serde only so `#[serde(rename)]` (which specta reads for the
633    // `type`/container names) is valid — the serde impls are never used.
634
635    /// The `"Feature"` value of a Feature's `type` member.
636    #[derive(Serialize, Deserialize, specta::Type)]
637    pub enum FeatureType {
638        Feature,
639    }
640
641    /// The `"FeatureCollection"` value of a collection's `type` member.
642    #[derive(Serialize, Deserialize, specta::Type)]
643    pub enum FeatureCollectionType {
644        FeatureCollection,
645    }
646
647    /// A GeoJSON Feature: a geometry `G` and its associated `properties` `P`
648    /// (RFC 7946 §3.2).
649    #[derive(Serialize, Deserialize, specta::Type)]
650    #[serde(rename = "Feature")]
651    pub struct Feature<G, P> {
652        #[serde(rename = "type")]
653        r#type: FeatureType,
654        geometry: G,
655        properties: P,
656        #[specta(type = Id, optional)]
657        id: Option<Id>,
658        #[specta(type = Bbox, optional)]
659        bbox: Option<Bbox>,
660    }
661
662    /// A GeoJSON FeatureCollection: a list of `Feature`s (RFC 7946 §3.3).
663    #[derive(Serialize, Deserialize, specta::Type)]
664    #[serde(rename = "FeatureCollection")]
665    pub struct FeatureCollection<G, P> {
666        #[serde(rename = "type")]
667        r#type: FeatureCollectionType,
668        features: Vec<Feature<G, P>>,
669        #[specta(type = Bbox, optional)]
670        bbox: Option<Bbox>,
671    }
672}