Skip to content

Commit 7d5f52b

Browse files
authored
Custom structs support id field (#256)
Note this would be a breaking change for anyone relying on the previous behavior which serialized "id" to/from the geojson `properties.id`
1 parent b237be3 commit 7d5f52b

File tree

3 files changed

+230
-2
lines changed

3 files changed

+230
-2
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
## Unreleased
44

5+
6+
* Potentially breaking: De/Serializing your custom structs with serde now maps your struct's `id` field to `Feature.id`, rather than to `Feature.properties.id`.
57
* Fix `geo_rect_conversion_test` to conform to the correctly-wound `Polygon` output from `geo_types::geometry::Rect.to_polygon`
68
* See https://github.com/georust/geojson/issues/257
79

10+
811
## 0.24.2 - 2025-02-24
912

1013
* Add `to_feature` to convert a single S: Serialize to a Feature

src/de.rs

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,18 @@ where
442442
if let JsonValue::Object(_) = value {
443443
hash_map.insert("geometry".to_string(), value);
444444
} else {
445-
return Err(Error::custom("GeoJSON Feature had a unexpected geometry"));
445+
return Err(Error::custom(
446+
"GeoJSON Feature had an unexpected `geometry`",
447+
));
448+
}
449+
} else if key == "id" {
450+
match &value {
451+
JsonValue::String(_) | JsonValue::Number(_) | JsonValue::Null => {
452+
hash_map.insert("id".to_string(), value);
453+
}
454+
_ => {
455+
return Err(Error::custom("GeoJSON Feature had an unexpected `id`"));
456+
}
446457
}
447458
} else if key == "properties" {
448459
if let JsonValue::Object(properties) = value {
@@ -451,7 +462,7 @@ where
451462
hash_map.insert(prop_key, prop_value);
452463
}
453464
} else {
454-
return Err(Error::custom("GeoJSON Feature had a unexpected geometry"));
465+
return Err(Error::custom("GeoJSON Feature had unexpected `properties`"));
455466
}
456467
} else {
457468
log::debug!("foreign members are not handled by Feature deserializer")
@@ -663,6 +674,141 @@ pub(crate) mod tests {
663674
assert!(feature.geometry.is_none())
664675
}
665676

677+
mod id_field {
678+
use super::*;
679+
680+
#[test]
681+
fn string_id() {
682+
#[derive(Deserialize)]
683+
struct MyStruct {
684+
#[serde(deserialize_with = "deserialize_geometry")]
685+
geometry: geo_types::Point<f64>,
686+
name: String,
687+
age: u64,
688+
id: String,
689+
}
690+
691+
let feature_string = json!({
692+
"type": "Feature",
693+
"id": "my-id-123",
694+
"geometry": {
695+
"type": "Point",
696+
"coordinates": [125.6, 10.1]
697+
},
698+
"properties": {
699+
"name": "Dinagat Islands",
700+
"age": 123
701+
}
702+
})
703+
.to_string();
704+
705+
let my_struct: MyStruct = deserialize_single_feature(feature_string.as_bytes())
706+
.expect("a valid feature collection");
707+
708+
assert_eq!(my_struct.geometry, geo_types::point!(x: 125.6, y: 10.1));
709+
assert_eq!(my_struct.id, "my-id-123");
710+
assert_eq!(my_struct.name, "Dinagat Islands");
711+
assert_eq!(my_struct.age, 123);
712+
}
713+
714+
#[test]
715+
fn numeric_id() {
716+
#[derive(Deserialize)]
717+
struct MyStruct {
718+
#[serde(deserialize_with = "deserialize_geometry")]
719+
geometry: geo_types::Point<f64>,
720+
name: String,
721+
age: u64,
722+
id: u64,
723+
}
724+
725+
let feature_string = json!({
726+
"type": "Feature",
727+
"id": 123,
728+
"geometry": {
729+
"type": "Point",
730+
"coordinates": [125.6, 10.1]
731+
},
732+
"properties": {
733+
"name": "Dinagat Islands",
734+
"age": 222
735+
}
736+
})
737+
.to_string();
738+
739+
let my_struct: MyStruct = deserialize_single_feature(feature_string.as_bytes())
740+
.expect("a valid feature collection");
741+
742+
assert_eq!(my_struct.geometry, geo_types::point!(x: 125.6, y: 10.1));
743+
assert_eq!(my_struct.id, 123);
744+
assert_eq!(my_struct.name, "Dinagat Islands");
745+
assert_eq!(my_struct.age, 222);
746+
}
747+
748+
#[test]
749+
fn optional_id() {
750+
#[allow(unused)]
751+
#[derive(Deserialize)]
752+
struct MyStruct {
753+
#[serde(deserialize_with = "deserialize_geometry")]
754+
geometry: geo_types::Point<f64>,
755+
name: String,
756+
age: u64,
757+
id: Option<u64>,
758+
}
759+
760+
let feature_string = json!({
761+
"type": "Feature",
762+
"id": 123,
763+
"geometry": {
764+
"type": "Point",
765+
"coordinates": [125.6, 10.1]
766+
},
767+
"properties": {
768+
"name": "Dinagat Islands",
769+
"age": 222
770+
}
771+
})
772+
.to_string();
773+
let my_struct: MyStruct = deserialize_single_feature(feature_string.as_bytes())
774+
.expect("a valid feature collection");
775+
assert_eq!(my_struct.id, Some(123));
776+
777+
let feature_string = json!({
778+
"type": "Feature",
779+
"geometry": {
780+
"type": "Point",
781+
"coordinates": [125.6, 10.1]
782+
},
783+
"properties": {
784+
"name": "Dinagat Islands",
785+
"age": 222
786+
}
787+
})
788+
.to_string();
789+
let my_struct: MyStruct = deserialize_single_feature(feature_string.as_bytes())
790+
.expect("a valid feature collection");
791+
assert_eq!(my_struct.id, None);
792+
793+
let feature_string = json!({
794+
"type": "Feature",
795+
"id": null,
796+
"geometry": {
797+
"type": "Point",
798+
"coordinates": [125.6, 10.1]
799+
},
800+
"properties": {
801+
"name": "Dinagat Islands",
802+
"age": 222
803+
}
804+
})
805+
.to_string();
806+
let my_struct: MyStruct = deserialize_single_feature(feature_string.as_bytes())
807+
.expect("a valid feature collection");
808+
assert_eq!(my_struct.id, None);
809+
}
810+
}
811+
666812
#[test]
667813
fn test_from_feature() {
668814
#[derive(Debug, PartialEq, Deserialize)]

src/ser.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,9 @@ where
480480
let mut map = serializer.serialize_map(Some(3))?;
481481
map.serialize_entry("type", "Feature")?;
482482
map.serialize_entry("geometry", &geometry)?;
483+
if json_object.contains_key("id") {
484+
map.serialize_entry("id", &json_object.remove("id"))?;
485+
}
483486
map.serialize_entry("properties", &json_object)?;
484487
map.end()
485488
}
@@ -740,6 +743,82 @@ mod tests {
740743
assert_eq!(actual_output, expected_output);
741744
}
742745

746+
#[test]
747+
fn with_id_field() {
748+
#[derive(Serialize)]
749+
struct MyStruct {
750+
#[serde(serialize_with = "serialize_geometry")]
751+
geometry: geo_types::Point<f64>,
752+
name: String,
753+
age: u64,
754+
id: &'static str,
755+
}
756+
757+
let my_struct = MyStruct {
758+
geometry: geo_types::point!(x: 125.6, y: 10.1),
759+
name: "Dinagat Islands".to_string(),
760+
age: 123,
761+
id: "my-id-123",
762+
};
763+
764+
let expected_output = json!({
765+
"type": "Feature",
766+
"id": "my-id-123",
767+
"geometry": {
768+
"type": "Point",
769+
"coordinates": [125.6, 10.1]
770+
},
771+
"properties": {
772+
"name": "Dinagat Islands",
773+
"age": 123
774+
}
775+
});
776+
777+
// Order might vary, so re-parse to do a semantic comparison of the content.
778+
let output_string = to_feature_string(&my_struct).expect("valid serialization");
779+
let actual_output = JsonValue::from_str(&output_string).unwrap();
780+
781+
assert_eq!(actual_output, expected_output);
782+
}
783+
784+
#[test]
785+
fn with_numeric_id_field() {
786+
#[derive(Serialize)]
787+
struct MyStruct {
788+
#[serde(serialize_with = "serialize_geometry")]
789+
geometry: geo_types::Point<f64>,
790+
name: String,
791+
age: u64,
792+
id: u64,
793+
}
794+
795+
let my_struct = MyStruct {
796+
geometry: geo_types::point!(x: 125.6, y: 10.1),
797+
name: "Dinagat Islands".to_string(),
798+
age: 123,
799+
id: 666,
800+
};
801+
802+
let expected_output = json!({
803+
"type": "Feature",
804+
"id": 666,
805+
"geometry": {
806+
"type": "Point",
807+
"coordinates": [125.6, 10.1]
808+
},
809+
"properties": {
810+
"name": "Dinagat Islands",
811+
"age": 123
812+
}
813+
});
814+
815+
// Order might vary, so re-parse to do a semantic comparison of the content.
816+
let output_string = to_feature_string(&my_struct).expect("valid serialization");
817+
let actual_output = JsonValue::from_str(&output_string).unwrap();
818+
819+
assert_eq!(actual_output, expected_output);
820+
}
821+
743822
#[test]
744823
fn test_to_feature() {
745824
#[derive(Serialize)]

0 commit comments

Comments
 (0)