有没有办法告诉Serde将结构体字段用作映射的键?

12

我有一个项目地图,希望将其序列化为一系列结构体列表,每个结构体都具有相应键的字段。

想象一下有一个这样的YAML文件:

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

同时还需要一个类似这样的对应结构:

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

我想将这些被命名的对象反序列化到一个 Vec<Item> 中,而不是一个 Map<String, Item>。项目名称(name_a,...)被放入 Item 对象的 name 字段中。

我尝试了以下方法:

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();

然而,这种方法行不通,会导致invalid type: map, expected a sequence的错误。

我更愿意避免创建一个临时的Map<String, PartialItem>,然后将其转换为Vec,我也不想实现另外一个PartialItem结构体。使用Option<String>作为name是可行的,但我认为这并不是最优解。


name存在重复值时,应该发生什么?它只会生成无效数据吗? - Shepmaster
好问题。我相信 YAML 建议重复的项按顺序被覆盖,因此使用最后一次出现的,其他出现的被删除。对于我的用例来说,这是完美的。 - Tim Visée
我可能会将其反序列化为 Map<String, PartialItem>,然后将其转换为 Vec<Item>;我猜你想避免创建瞬态的 Map - Shepmaster
正确。我也不想实现额外的 PartialItem 结构体。使用 Option<String> 作为名称是可能的,尽管我认为这不是最佳选择。如果没有更好的选择,也许暂时的 Map 是我必须采取的方法。 - Tim Visée
2
你可能需要自己实现Deserialize https://serde.rs/deserialize-map.html - PitaJ
YAML规范明确指出映射键必须是唯一的。重复的键是无效的,尽管有些解析器/加载器(甚至有些是悄悄地)允许它们存在。 - Anthon
2个回答

7

一种方法是自己进行地图的反序列化

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);
}

输出:

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

如果您不想使用Somefield结构体,也可以使用以下方法:
#[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));
}

但是这可能会添加一些无用的复制内容。


1
Item::name 字段定义默认值。
#[derive(Debug, Serialize, Deserialize)]
struct Item {
    #[serde(default)]
    name: String,
    some_field: usize,
}

使用这个技巧,Item既可以用于反序列化,也可以转换为VecItems:
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();

2
OP 表示他们想要“避免创建临时 Map”。 - Shepmaster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接