I have a struct which, depending on options, I want to serialize to json with different subsets of the fields. However, my existing solution leads to a large amount of hard-to-maintain boilerplate. I'm sure there must be an elegant solution, but as a newcomer to rust I'm not sure what it is.
The context of the code is that it is for an API. It queries data (called observations in the code below) from a database (using diesel), then serves it with rocket.rs. Here, I am focused on the layer that formats the JSON object. The core querying code is shared, but depending on parameters passed to the API, some fields should be optionally excluded. The code shared below is that formatting code that selects a subset of fields and converts it to a JSON-serializable object.
Approaches I've considered:
- Converting the struct to a new struct with a subset of the fields, and converting that to a
serde_json::Value
. This is my current solution. However, this leads to a ton of boilerplate. - Making the fields be
Option
s. This won't work, as it will add unnecessary"field": null
strings to the serialized result (which increases the size of the end result, defeating the purpose of optionally excluding fields). - Implementing a custom serialize method instead of just using
#[derive(Serialize)]
. However, as far as I can tell, there's no way to pass options into the call to serialize.
What's the best-practice way to serialize a struct to JSON with a dynamic subset of the fields? Feedback is welcome on other aspects of the code too, but I see that as the biggest flaw in the code.
Example code (as you can see, it has a lot of boilerplate).
use serde_json::json;
// Main function transforms the data into something JSON-serializable based on the options
// It doesn't matter what the return type is as long it's JSON-serializable
pub fn query_observations(include_ids : bool, include_mission_name : bool) -> Vec<serde_json::Value> {
let observations : Vec<ObservationWithMetadata> = vec![]; // in the real world, this gets read from somewhere
observations.drain(..).map( |observation| {
if include_ids && include_mission_name {
json!(observation)
} else if include_ids {
json!(ObservationWithIds::from(observation))
} else if include_mission_name {
json!(ObservationWithMissionName::from(observation))
} else {
json!(Observation::from(observation))
}
}).collect()
}
// All the different variants...
#[derive(Serialize, Deserialize, Debug)]
pub struct ObservationWithMetadata {
pub id : i64,
pub mission_id : String,
pub mission_name : Option<String>,
pub timestamp : f64,
pub altitude : Option<f64>,
pub latitude : Option<f64>,
pub longitude : Option<f64>,
pub pressure : Option<f64>,
pub speed_x : Option<f64>,
pub speed_y : Option<f64>,
pub temperature : Option<f64>,
pub humidity : Option<f64>
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Observation {
pub timestamp : f64,
pub altitude : Option<f64>,
pub latitude : Option<f64>,
pub longitude : Option<f64>,
pub pressure : Option<f64>,
pub speed_x : Option<f64>,
pub speed_y : Option<f64>,
pub temperature : Option<f64>,
pub humidity : Option<f64>
}
impl From<ObservationWithMetadata> for Observation {
fn from(observation: ObservationWithMetadata) -> Self {
Observation {
timestamp: observation.timestamp,
altitude: observation.altitude,
latitude: observation.latitude,
longitude: observation.longitude,
pressure: observation.pressure,
speed_x: observation.speed_x,
speed_y: observation.speed_y,
temperature: observation.temperature,
humidity: observation.humidity
}
}
}
#[allow(dead_code)]
#[derive(Serialize, Deserialize, Debug)]
pub struct ObservationWithIds {
pub id : i64,
pub mission_id : String,
pub timestamp : f64,
pub altitude : Option<f64>,
pub latitude : Option<f64>,
pub longitude : Option<f64>,
pub pressure : Option<f64>,
pub speed_x : Option<f64>,
pub speed_y : Option<f64>,
pub temperature : Option<f64>,
pub humidity : Option<f64>
}
impl From<ObservationWithMetadata> for ObservationWithIds {
fn from(observation: ObservationWithMetadata) -> Self {
ObservationWithIds {
id: observation.id,
mission_id: observation.mission_id,
timestamp: observation.timestamp,
altitude: observation.altitude,
latitude: observation.latitude,
longitude: observation.longitude,
pressure: observation.pressure,
speed_x: observation.speed_x,
speed_y: observation.speed_y,
temperature: observation.temperature,
humidity: observation.humidity
}
}
}
#[allow(dead_code)]
#[derive(Serialize, Deserialize, Debug)]
pub struct ObservationWithMissionName {
pub mission_name : Option<String>,
pub timestamp : f64,
pub altitude : Option<f64>,
pub latitude : Option<f64>,
pub longitude : Option<f64>,
pub pressure : Option<f64>,
pub speed_x : Option<f64>,
pub speed_y : Option<f64>,
pub temperature : Option<f64>,
pub humidity : Option<f64>
}
impl From<ObservationWithMetadata> for ObservationWithMissionName {
fn from(observation: ObservationWithMetadata) -> Self {
ObservationWithMissionName {
mission_name: observation.mission_name,
timestamp: observation.timestamp,
altitude: observation.altitude,
latitude: observation.latitude,
longitude: observation.longitude,
pressure: observation.pressure,
speed_x: observation.speed_x,
speed_y: observation.speed_y,
temperature: observation.temperature,
humidity: observation.humidity
}
}
}