- [x] I agree to follow the project's code of conduct.
- [x] I added an entry to
CHANGES.md
if knowledge of this change could be valuable to users.
This is an attempt to improve the ergonomics of parsing GeoJson using serde (FIXES https://github.com/georust/geojson/issues/184) .
~~This PR is a draft because there are a lot of error paths related to invalid parsing that I'd like to add tests for, but I first wanted to check in on overall direction of the API. What do people think?~~ I think this is ready for review!
Examples
Given some geojson like this:
let feature_collection_string = r#"{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [125.6, 10.1]
},
"properties": {
"name": "Dinagat Islands",
"age": 123
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [2.3, 4.5]
},
"properties": {
"name": "Neverland",
"age": 456
}
}
]
}"#
.as_bytes();
let io_reader = std::io::BufReader::new(feature_collection_string);
Before
Deserialization
You used to parse it like this:
use geojson:: FeatureIterator;
let feature_reader = FeatureIterator::new(io_reader);
for feature in feature_reader.features() {
let feature = feature.expect("valid geojson feature");
let name = feature.property("name").unwrap().as_str().unwrap();
let age = feature.property("age").unwrap().as_u64().unwrap();
let geometry = feature.geometry.value.try_into().unwrap();
if name == "Dinagat Islands" {
assert_eq!(123, age);
assert_matches!(geometry, geo_types::Point::new(125.6, 10.1).into());
} else if name == "Neverland" {
assert_eq!(456, age);
assert_matches!(geometry, geo_types::Point::new(2.3, 4.5).into());
} else {
panic!("unexpected name: {}", name);
}
}
Serialization
Then, to write it back to geojson, you'd have to either do all your processing strictly with the geojson types, or somehow convert your entities from and back to one of the GeoJson entities:
Something like:
// The implementation of this method is potentially a little messy / boilerplatey
let feature_collection: geojson::FeatureCollection = some_custom_method_to_convert_to_geojson(&my_structs);
// This part is easy enough though
serde_json::to_string(&geojson);
After
But now you also have the option of parsing it into your own declarative struct using serde like this:
Declaration
use geojson::{ser::serialize_geometry, de::deserialize_geometry};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct MyStruct {
// You can parse directly to geo_types via these helpers, otherwise this field will need to be a `geojson::Geometry`
#[serde(serialize_with = "serialize_geometry", deserialize_with = "deserialize_geometry")]
geometry: geo_types::Point<f64>,
name: String,
age: u64,
}
Deserialization
for feature in geojson::de::deserialize_feature_collection::<MyStruct>(io_reader).unwrap() {
let my_struct = feature.expect("valid geojson feature");
if my_struct.name == "Dinagat Islands" {
assert_eq!(my_struct.age, 123);
assert_eq!(my_struct.geometry, geo_types::Point::new(125.6, 10.1));
} else if my_struct.name == "Neverland" {
assert_eq!(my_struct.age, 456);
assert_eq!(my_struct.geometry, geo_types::Point::new(2.3, 4.5));
} else {
panic!("unexpected name: {}", my_struct.name);
}
}
Serialization
let my_structs: Vec<MyStruct> = get_my_structs();
geojson::ser::to_feature_collection_writer(writer, &my_structs).expect("valid serialization");
Caveats
Performance
Performance currently isn't great. There's a couple of things which seem ridiculous in the code that I've marked with PERF:
that I don't have an immediate solution for. This is my first time really diving into the internals of serde and it's kind of a lot! My hope is that performance improvements would be possible with no or little changes to the API.
Some specific numbers (from some admittedly crude benchmarks):
Old Deserialization:
FeatureReader::features (countries.geojson)
time: [5.8497 ms 5.8607 ms 5.8728 ms]
New Deserialization:
FeatureReader::deserialize (countries.geojson)
time: [7.1702 ms 7.1865 ms 7.2035 ms]
Old serialization:
serialize geojson::FeatureCollection struct (countries.geojson)
time: [3.1471 ms 3.1552 ms 3.1637 ms]
New serialization:
serialize custom struct (countries.geojson)
time: [3.8076 ms 3.8144 ms 3.8219 ms]
So the new "ergonomic" serialization/deserialization takes about 1.2x the time as the old way. Though it's actually probably a bit better than that because with the new form you have all your data ready to use. With the old way, you still need to go through this dance before you can start your analysis:
let name = feature.property("name").unwrap().as_str().unwrap();
let age = feature.property("age").unwrap().as_u64().unwrap();
let geometry = feature.geometry.value.try_into().unwrap();
Anyway, I think this kind of speed difference is well worth the improved usability for my use cases.
Foreign Members
This doesn't support anything besides geometry
and properties
- e.g. foreign members are dropped. I'm hopeful that this is useful even with that limitation, but if not, maybe we can think of a way to accommodate most people's needs.