Search code examples
rustyamlserde

How to Deserialize a Map into a Vector of Enums with Serde in Rust?


I am working on a Rust project where I need to deserialize YAML configuration data into a more structured Rust type. The YAML data represents output specifications for different data sinks (like CSV and JSON files) and looks something like this:

output_data_specs:
  csv:
    file_path: "/output-data.csv"
    delimiter: ","
  json:
    file_path: "/output/"
    another: "foo"

I want to deserialize this data into a Rust Vec<Sink>, where Sink is an enum that can represent either a CSV sink or a JSON sink:

pub enum Sink {
    Csv(CsvSink),
    Json(JsonSink),
}

pub struct CsvSink {
    file_path: String,
    append: bool,
}

pub struct JsonSink {
    file_path: String,
    another: String
}

I'm looking for a way to implement this with Serde, it should deserialize the map of output data specs map into a list of Sinks in something like:

pub struct InferenceConfig {
    pub name: String,
    pub output_data_specs: Vec<Sink>,
}

I'd preferably like to implement this without a custom deserializer. Any examples or pointers to relevant documentation would be very helpful.


Solution

  • If you can change your Map to a List (simply add - in front of csv and json) — which it seems you want to do, since output_data_specs is a Vec<Sink> — you can use #[serde(with = "serde_yaml::with::singleton_map_recursive")] to use the keys csv and json as enum tags in serde’s default enum representation.

    use serde::Deserialize;
    
    #[derive(Debug, Deserialize)]
    #[serde(rename_all = "snake_case")]
    pub enum Sink {
        Csv(CsvSink),
        Json(JsonSink),
    }
    
    #[derive(Debug, Deserialize)]
    pub struct CsvSink {
        file_path: String,
        append: bool,
        delimiter: String,
    }
    
    #[derive(Debug, Deserialize)]
    pub struct JsonSink {
        file_path: String,
        another: String,
    }
    
    #[derive(Debug, Deserialize)]
    pub struct InferenceConfig {
        pub name: String,
        #[serde(with = "serde_yaml::with::singleton_map_recursive")]
        pub output_data_specs: Vec<Sink>,
    }
    
    fn main() {
        let s = r#"
            name: "xyz"
            output_data_specs:
                - csv:
                    file_path: "/output-data.csv"
                    append: true
                    delimiter: ","
                - json:
                    file_path: "/output/"
                    another: "foo"
            "#;
        let inference_config = serde_yaml::from_str::<InferenceConfig>(s).unwrap();
        println!("{inference_config:#?}");
    }
    
    InferenceConfig {
        name: "xyz",
        output_data_specs: [
            Csv(
                CsvSink {
                    file_path: "/output-data.csv",
                    append: true,
                    delimiter: ",",
                },
            ),
            Json(
                JsonSink {
                    file_path: "/output/",
                    another: "foo",
                },
            ),
        ],
    }