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}