Can I make serde set default property value from parent HashMap key?

I'm trying to use the wonderful serde crate to parse YAML and set a default value from the "parent" HashMap key. I managed to use #[serde(default = "function_name")] to set static default values, but I'm not sure how to look up data that was previously deserialized.

Is that possible?

I added a small example below:

#[derive(Deserialize, Debug)]
pub struct Root {
  pub maps: HashMap<String, Map>,
}

fn default_false_value() -> bool {false}

#[derive(Deserialize, Debug)]
pub struct Map {
  // How can I annotate a function here default name to the above HashMap key if missing in the YAML
  pub name: String,
  #[serde(default = "default_false_value")]
  pub collisions: bool,
}

YAML example

maps:
  wood:
    #name: wood <-- how can I allow for omitting this and default to the key in line above?
    collisions: true

I don't think it's possible with built-in Deserialize derive. If you wrote your own deserializer implementation to smuggle some context trough, then maybe.

But easier options are:

  • Use Option<String> and look the name up later when using the struct.

  • Make two copies of the struct definition โ€” one for deserialization with optional fields, and one processed final with non-optional fields. Translate one to another after deserialization.

2 Likes

You will need to write you own deserializer, because, when you think about it, Map, in and of itself, cannot be Deserialize. It can only be deserialized when within the bigger picture. So, if you want a helper auto-Deserialize type, that type would be one with a name: Option<String>:

  • Relevant logic:
    #[derive(Deserialize)]
    struct RawMap {
        #[serde(default)]
        name: Option<String>,
    
        #[serde(default = "default_false_value")]
        collisions: bool,
    }
    
    while let Some((key, value)) = access.next_entry::<String, RawMap>()? {
        let value = Map {
            collisions: value.collisions,
            name: value.name.unwrap_or_else(|| key.clone()),
        };
        map.insert(key, value);
    }
    

Full snippet (manual impl of Deserialize based off Deserialize for custom map type ยท Serde):

#[derive(Deserialize, Debug)]
pub struct Root {
    #[serde(deserialize_with = "deserialize_maps")]
    pub maps: HashMap<String, Map>,
}

fn default_false_value () -> bool { false }

#[derive(Debug, /* Deserialize */)]
pub
struct Map {
    pub
    name: String,

    pub
    collisions: bool,
}

fn deserialize_maps<'de, D> (deserializer: D)
  -> Result<HashMap<String, Map>, D::Error>
where
    D : ::serde::de::Deserializer<'de>,
{
    // Based off https://serde.rs/deserialize-map.html
    use ::serde::de::*;

    type MyMap = HashMap<String, Map>;

    struct MyMapVisitor;
    
    impl<'de> Visitor<'de> for MyMapVisitor {
        // The type that our Visitor is going to produce.
        type Value = MyMap;
    
        // Format a message stating what data this Visitor expects to receive.
        fn expecting(&self, formatter: &mut ::core::fmt::Formatter)
          -> ::core::fmt::Result
        {
            formatter.write_str("a map")
        }
    
        // Deserialize MyMap from an abstract "map" provided by the
        // Deserializer. The MapAccess input is a callback provided by
        // the Deserializer to let us see each entry in the map.
        fn visit_map<M>(self, mut access: M)
          -> Result<Self::Value, M::Error>
        where
            M : MapAccess<'de>,
        {
            let mut map = MyMap::with_capacity(access.size_hint().unwrap_or(0));

            #[derive(Deserialize)]
            struct RawMap {
                #[serde(default)]
                name: Option<String>,
            
                #[serde(default = "default_false_value")]
                collisions: bool,
            }
    
            // While there are entries remaining in the input, add them
            // into our map.
            while let Some((key, value)) = access.next_entry()? {
                // type-hint `key`
                let _: String = key;
                // Extract fields and type-hint `value`
                let RawMap { name, collisions } = value;
                let value = Map {
                    collisions,
                    name: name.unwrap_or_else(|| key.clone()),
                };
                map.insert(key, value);
            }
    
            Ok(map)
        }
    }
    
    deserializer.deserialize_map(MyMapVisitor)
}
2 Likes

Thank you for the answer, I'll consider if Option or custom parser makes most sense. When thinking more about the problem, it seem more and more clear to me that I might consider duplicating some types having one for raw parsing and one for the final output. I will anyway have parsing code.

Thank you for a nice example for a custom parser, some extra code to get it working but I like the result.

Now I have two good options to choose from in both anwers. :slight_smile:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.