georust / geojson

Library for serializing the GeoJSON vector GIS file format
https://crates.io/crates/geojson
Apache License 2.0
280 stars 60 forks source link

Issue with serializing custom struct #219

Closed simtrax closed 1 year ago

simtrax commented 1 year ago

Hi, I'm working on serializing a struct. I have my own properties JsonObject since the struct can have a variable set of properties. I'll show some of the code I have written. I'm fairly new to Rust so there's definitely a risk of me misunderstanding something.

use geo::Polygon;
use geojson::{Value, JsonObject, JsonValue, Feature};
use serde::ser::{Serialize, Serializer};

pub struct Cell {
    pub geometry: Polygon,
    pub center_coords: [f64; 2],
    pub deleted: bool,
    pub properties: JsonObject,
}

impl Serialize for Cell {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let feature = Feature {
            bbox: None,
            geometry: Some(Value::from(&self.geometry).into()),
            id: None,
            properties: Some(self.properties.clone()),
            foreign_members: None,
        };

        Feature::serialize(&feature, serializer)
    }
}

The issue is that when I want to create a FeatureCollection string the GeoJSON data is behaving strange.

let cells = vec![cell];
let output_geojson = geojson::ser::to_feature_collection_string(&cells).unwrap();
println!("{:?}", output_geojson);

Outputs:

"{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"geometry\":{\"coordinates\":[[[392060.0,6150440.0],[392080.0,6150440.0],[392080.0,6150420.0],[392060.0,6150420.0],[392060.0,6150440.0]]],\"type\":\"Polygon\"},\"properties\":{\"properties\":{\"k_al\":12.972,\"p_al\":6.882,\"ph\":6.497},\"type\":\"Feature\"}}]}"

As you can see theres a nested properties object inside properties. And the \"type\":\"Feature\" resides inside properties.

When using this code I get a nice looking feature at least:

let cells = vec![cell];
let result = serde_json::to_string_pretty(&cells);
println!("{}", result.unwrap());

Output:

[
  {
    "geometry": {
      "coordinates": [
        [
          [
            392060.0,
            6150440.0
          ],
          [
            392080.0,
            6150440.0
          ],
          [
            392080.0,
            6150420.0
          ],
          [
            392060.0,
            6150420.0
          ],
          [
            392060.0,
            6150440.0
          ]
        ]
      ],
      "type": "Polygon"
    },
    "properties": {
      "k_al": 12.972,
      "p_al": 6.882,
      "ph": 6.497
    },
    "type": "Feature"
  }
]

When checking the source code that I call in the first example I would think it should work as intended, but it doesn't. I would be glad to get some answers to why I get a nested properties object when I have a Serialize implementation for Cell that explains how to serialize it, that does not put properties in a nested way.

michaelkirk commented 1 year ago

Hi @simtrax - maybe you figured out a solution since you closed this issue, but in case you or someone else still has this question, these docs might be useful: https://docs.rs/geojson/latest/geojson/ser/index.html

All fields in your struct other than geometry will be serialized as properties of the GeoJSON Feature.

This is related to the front page docs:

There are two primary ways to use this crate.

The first, most general, approach is to write your code to deal in terms of these structs from the GeoJSON spec. This allows you to access the full expressive power of GeoJSON with the speed and safety of Rust.

Alternatively, and commonly, if you only need geometry and properties (and not, e.g. foreign members), you can bring your own types, and use this crate’s serde integration

So, if you are going the "bring your own types" route, you'll need to percolate up the loosely typed "properties" map to some top level typed fields on your Cell struct, something like:

pub struct Cell {
    pub geometry: Polygon,
    pub center_coords: [f64; 2],
    pub deleted: bool,
-   pub properties: JsonObject,
+   pub k_al: f64,
+   pub p_al: f64,
+   pub ph: f64
}

If you don't want center_coords, or deleted to be in the output geojson, serde has some attributes for that.

If you are unable to describe your type in the rust type system like this (e.g. if the members or types of properties are very dynamic or if you need access to some other part of the feature like bbox or something), then you'll need to go the more pedantic "write your code to deal in terms of these structs from the GeoJSON spec" route, like this example: https://docs.rs/geojson/latest/geojson/index.html#writing