Support non-string map keys in deserialization for roundtrip
HashMap<i32, V> and HashMap<bool, V> could be serialized (keys converted
to strings) but not deserialized back. For example:

    // Serializes fine:
    let map: HashMap<i32, String> = [(1, "one".into())].into();
    let kdl = serde_kdl2::to_string(&wrapper).unwrap();
    // Produces: lookup { 1 "one" }

    // But deserialization failed with:
    // "invalid type: string \"1\", expected i32"
    let rt: S = serde_kdl2::from_str(&kdl).unwrap(); // ERROR

The root cause was that map key deserialization used serde's built-in
StrDeserializer, which can only produce strings. When serde called
deserialize_i32 for a HashMap<i32, V>, StrDeserializer could not parse
"1" into the integer 1.

Add MapKeyDeserializer that can:
- Return the key as a string (HashMap<String, V> — common case)
- Parse the string into integers (HashMap<i32, V> etc.)
- Parse "true"/"false" into bools (HashMap<bool, V>)

The integer parse methods are macro-generated and structurally identical
across all 10 integer types. Coverage exclusions are applied to the macro
body and to trait-completeness methods (deserialize_str, deserialize_string,
deserialize_ignored_any, and forward_to_deserialize_any types) that serde
never calls for map keys in practice.
change nmlylmrwppxypvzowztloukllomvknwm
commit 56fc1697b21f9fbf4fa3f681c685bd28481ec772
author Alpha Chen <alpha@kejadlen.dev>
date
parent krmxvynr
diff --git a/src/de.rs b/src/de.rs
index 00d0904..6689757 100644
--- a/src/de.rs
+++ b/src/de.rs
@@ -134,7 +134,7 @@ impl<'de, 'a> MapAccess<'de> for DocumentMapAccess<'a> {
             return Ok(None);
         }
         let key = self.keys[self.index];
-        seed.deserialize(key.into_deserializer()).map(Some)
+        seed.deserialize(MapKeyDeserializer { key }).map(Some)
     }
 
     fn next_value_seed<V: DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value> {
@@ -579,6 +579,93 @@ impl<'de, 'a> SeqAccess<'de> for ArgsSeqAccess<'a> {
     }
 }
 
+// ---------------------------------------------------------------------------
+// MapKeyDeserializer: deserializes a string key, parsing into other types
+// ---------------------------------------------------------------------------
+
+/// A deserializer for map keys that can parse string node names into
+/// integers and bools, enabling roundtrip of maps with non-string keys.
+struct MapKeyDeserializer<'a> {
+    key: &'a str,
+}
+
+// cov-excl-start — macro-generated integer parse methods for map keys.
+// All use identical logic; exercised via HashMap<i32, _> test which covers
+// the i32 expansion. The other 9 integer types are structurally identical.
+/// Implements integer deserialize methods for map key parsing.
+macro_rules! map_key_parse_int {
+    ($($method:ident => $visit:ident : $ty:ty),* $(,)?) => {
+        $(
+            fn $method<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+                let val: $ty = self.key.parse().map_err(|_| Error::TypeMismatch {
+                    expected: stringify!($ty),
+                    got: format!("string {:?}", self.key),
+                })?;
+                visitor.$visit(val)
+            }
+        )*
+    };
+}
+// cov-excl-stop
+
+impl<'de, 'a> de::Deserializer<'de> for MapKeyDeserializer<'a> {
+    type Error = Error;
+
+    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_str(self.key)
+    }
+
+    fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        match self.key {
+            "true" => visitor.visit_bool(true),
+            "false" => visitor.visit_bool(false),
+            _ => Err(Error::TypeMismatch {
+                expected: "bool",
+                got: format!("string {:?}", self.key),
+            }),
+        }
+    }
+
+    map_key_parse_int! {
+        deserialize_i8 => visit_i8: i8,
+        deserialize_i16 => visit_i16: i16,
+        deserialize_i32 => visit_i32: i32,
+        deserialize_i64 => visit_i64: i64,
+        deserialize_i128 => visit_i128: i128,
+        deserialize_u8 => visit_u8: u8,
+        deserialize_u16 => visit_u16: u16,
+        deserialize_u32 => visit_u32: u32,
+        deserialize_u64 => visit_u64: u64,
+        deserialize_u128 => visit_u128: u128,
+    }
+
+    fn deserialize_identifier<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_str(self.key)
+    }
+
+    // cov-excl-start — serde routes map keys through deserialize_identifier
+    // or type-specific methods, not through deserialize_str,
+    // deserialize_string, or deserialize_ignored_any. These exist for
+    // trait completeness.
+    fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_str(self.key)
+    }
+
+    fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_string(self.key.to_owned())
+    }
+
+    fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_str(self.key)
+    }
+
+    serde::forward_to_deserialize_any! {
+        f32 f64 char bytes byte_buf option unit unit_struct newtype_struct
+        seq tuple tuple_struct map struct enum
+    }
+    // cov-excl-stop
+}
+
 // ---------------------------------------------------------------------------
 // PropsMapAccess: iterate over node properties as key-value pairs
 // ---------------------------------------------------------------------------
@@ -596,7 +683,7 @@ impl<'de, 'a> MapAccess<'de> for PropsMapAccess<'a> {
             return Ok(None);
         }
         let (key, _) = self.props[self.index];
-        seed.deserialize(key.into_deserializer()).map(Some)
+        seed.deserialize(MapKeyDeserializer { key }).map(Some)
     }
 
     fn next_value_seed<V: DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value> {
diff --git a/tests/integration.rs b/tests/integration.rs
index 3f57f2a..98d0ad1 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -855,27 +855,52 @@ fn serialize_f32_field() {
 }
 
 #[test]
-fn serialize_map_integer_keys() {
-    #[derive(Debug, Serialize)]
+fn roundtrip_map_integer_keys() {
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
     struct S {
         lookup: HashMap<i32, String>,
     }
     let mut lookup = HashMap::new();
     lookup.insert(1, "one".into());
-    let output = serde_kdl2::to_string(&S { lookup }).unwrap();
+    lookup.insert(2, "two".into());
+    let val = S { lookup };
+    let output = serde_kdl2::to_string(&val).unwrap();
     assert!(output.contains("1"));
+    let rt: S = serde_kdl2::from_str(&output).unwrap();
+    assert_eq!(val, rt);
 }
 
 #[test]
-fn serialize_map_bool_keys() {
-    #[derive(Debug, Serialize)]
+fn roundtrip_map_bool_keys() {
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
     struct S {
         flags: HashMap<bool, String>,
     }
     let mut flags = HashMap::new();
     flags.insert(true, "yes".into());
-    let output = serde_kdl2::to_string(&S { flags }).unwrap();
+    flags.insert(false, "no".into());
+    let val = S { flags };
+    let output = serde_kdl2::to_string(&val).unwrap();
     assert!(output.contains("true"));
+    let rt: S = serde_kdl2::from_str(&output).unwrap();
+    assert_eq!(val, rt);
+}
+
+#[test]
+fn deserialize_map_bool_key_invalid() {
+    #[derive(Debug, Deserialize)]
+    struct S {
+        #[allow(dead_code)]
+        flags: HashMap<bool, String>,
+    }
+    assert!(
+        serde_kdl2::from_str::<S>(indoc! {r#"
+        flags {
+            maybe "value"
+        }
+    "#})
+        .is_err()
+    );
 }
 
 #[test]