13

I have a map of items that I would like to serialize to a list of structs, each having a field for the corresponding key.

Imagine having a YAML file like this:

name_a: some_field: 0 name_b: some_field: 0 name_c: some_field: 0 

And a corresponding structure like this:

struct Item { name: String, some_field: usize, } 

I would like to deserialize the named items into a Vec<Item> instead of a Map<String, Item>. The item names (name_a, ...) are put into the name field of the Item objects.

I've attempted the following:

extern crate serde_yaml; use std::fs::read_to_string; let contents = read_to_string("file.yml").unwrap(); let items: Vec<Item> = serde_yaml::from_str(&contents).unwrap(); 

This however doesn't work and produces the invalid type: map, expected a sequence error.

I'd prefer to avoid creating a transient Map<String, PartialItem> that is converted to a Vec, and I would also prefer not to implement an additional PartialItem struct. Using an Option<String> as name would be possible, although I don't think this is optimal.

6
  • What should happen when there are duplicate values for name? Should it just generate invalid data? Commented Nov 2, 2018 at 17:29
  • Good question. I believe YAML suggests duplicate items get overwritten in order, thus the last occurrence is used, other occurrences are dropped. For my use case, this is perfect. Commented Nov 2, 2018 at 17:32
  • I'd probably just deserialize to a Map<String, PartialItem> and then transform it to a Vec<Item>; I assume you are looking to avoid the transient Map being created? Commented Nov 2, 2018 at 17:38
  • Correct. And I would also prefer not to implement an additional PartialItem struct. Using an Option<String> as name would be possible for this, although I don't think this is optimal. Maybe the transient Map is what I must go for if no better option is available. Commented Nov 2, 2018 at 18:06
  • 2
    You'll probably have to implement Deserialize it yourself serde.rs/deserialize-map.html Commented Nov 2, 2018 at 18:24

3 Answers 3

7

One way is to deserialize the map yourself:

use std::fmt; use serde::de::{Deserialize, Deserializer, MapAccess, Visitor}; use serde_derive::Deserialize; struct ItemMapVisitor {} impl ItemMapVisitor { fn new() -> Self { Self {} } } #[derive(Debug, Deserialize)] struct SomeField { some_field: u32, } #[derive(Debug)] struct Item { name: String, some_field: u32, } #[derive(Debug)] struct VecItem(Vec<Item>); impl Item { fn new(name: String, some_field: u32) -> Self { Self { name, some_field } } } impl<'de> Visitor<'de> for ItemMapVisitor { type Value = VecItem; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("name: somefield:") } fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error> where M: MapAccess<'de>, { let mut items = Vec::with_capacity(access.size_hint().unwrap_or(0)); while let Some((key, value)) = access.next_entry::<String, SomeField>()? { items.push(Item::new(key, value.some_field)); } Ok(VecItem(items)) } } impl<'de> Deserialize<'de> for VecItem { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { deserializer.deserialize_map(ItemMapVisitor::new()) } } fn main() { let contents = r#" name_a: some_field: 0 name_b: some_field: 1 name_c: some_field: 2 "#; let items: VecItem = serde_yaml::from_str(&contents).unwrap(); println!("{:#?}", items); } 

Output:

VecItem( [ Item { name: "name_a", some_field: 0 }, Item { name: "name_b", some_field: 1 }, Item { name: "name_c", some_field: 2 } ] ) 

If you don't want of Somefield structure. You could also use this:

#[derive(Debug, Deserialize)] struct Item { #[serde(skip)] name: String, some_field: u32, } while let Some((key, value)) = access.next_entry::<String, Item>()? { items.push(Item::new(key, value.some_field)); } 

But this could add some useless copy.

Sign up to request clarification or add additional context in comments.

Comments

5

Use serde_with and its #[serde(rename = "$key$")].

use serde_with::{serde_as, KeyValueMap}; #[derive(Serialize, Deserialize)] struct Item { #[serde(rename = "$key$")] name: String, some_field: usize, } // This would be the enclosing type #[serde_as] #[derive(Serialize, Deserialize)] struct MyMap( #[serde_as(as = "KeyValueMap<_>")] Vec<Item>, ); 

Comments

1

Define a default value for Item::name field

#[derive(Debug, Serialize, Deserialize)] struct Item { #[serde(default)] name: String, some_field: usize, } 

With this trick Itemcan be used both for deserializing and for transforming to a Vec of Items:

let contents = read_to_string("file.yml").unwrap(); let items: HashMap<String, Item> = serde_yaml::from_str(&contents).unwrap(); let slist: Vec<Item> = items .into_iter() .map(|(k, v)| Item { name: k, some_field: v.some_field, }) .collect(); 

1 Comment

OP states that they want to "avoid the transient Map being created"

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.