Repository: tafia/quick-xml
Pull request #873 (2025-06-25 16:51:13)

Allow attributes in the `xml` namespace to roundtrip with serde
This fixes [#841](https://github.com/tafia/quick-xml/issues/841).

When deserializing attribute names, this version prevents the `xml:` namespace prefix to be split off.

I am admittedly not familiar with XML namespaces at all and have taken [this reasoning](https://github.com/tafia/quick-xml/issues/841#issuecomment-2600865912) at face value. If this is not a desirable feature after all, please let me know!


Patch:
diff --git a/Changelog.md b/Changelog.md
index 8b18c0c..f535034 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -40,6 +40,7 @@ types should preserve whitespace, while all other primitives have collapse behav
 - [#868]: Allow to have both `$text` and `$value` special fields in one struct. Previously
   any text will be recognized as `$value` field even when `$text` field is also presented.
 - [#868]: Skip text events when deserialize a sequence of items overlapped with text (including CDATA).
+- [#841]: Do not strip `xml` prefix from the attributes when map them to struct fields in `Deserializer`.
 
 ### Misc Changes
 
@@ -50,6 +51,7 @@ types should preserve whitespace, while all other primitives have collapse behav
 
 [#285]: https://github.com/tafia/quick-xml/issues/285
 [#766]: https://github.com/tafia/quick-xml/pull/766
+[#841]: https://github.com/tafia/quick-xml/issues/841
 [#863]: https://github.com/tafia/quick-xml/pull/863
 [#868]: https://github.com/tafia/quick-xml/pull/868
 [general entity]: https://www.w3.org/TR/xml11/#gen-entity
diff --git a/src/de/key.rs b/src/de/key.rs
index a4d022b..aa19029 100644
--- a/src/de/key.rs
+++ b/src/de/key.rs
@@ -42,10 +42,12 @@ fn decode_name<'n>(name: QName<'n>, decoder: Decoder) -> Result<Cow<'n, str>, De
 /// - if it is an [`attribute`] name, put `@` in front of the identifier
 /// - if it is a namespace binding (`xmlns` or `xmlns:xxx`) put the decoded name
 ///   to the identifier
+/// - if it is an attribute in the `xml` namespace, put the decoded name
+///   to the identifier
 /// - put the decoded [`local_name()`] of a name to the identifier
 ///
-/// The final identifier looks like `[@]local_name`, or `@xmlns`, or `@xmlns:binding`
-/// (where `[]` means optional element).
+/// The final identifier looks like `[@]local_name`, or `@xmlns`, or `@xmlns:binding` or
+/// `xml:attribute` (where `[]` means optional element).
 ///
 /// The deserializer also supports deserializing names as other primitive types:
 /// - numbers
@@ -89,8 +91,15 @@ impl<'i, 'd> QNameDeserializer<'i, 'd> {
         if name.as_namespace_binding().is_some() {
             decoder.decode_into(name.into_inner(), key_buf)?;
         } else {
-            let local = name.local_name();
-            decoder.decode_into(local.into_inner(), key_buf)?;
+            // https://github.com/tafia/quick-xml/issues/841
+            // we also want to map to the full name for `xml:xxx`, because `xml:xxx` attributes
+            // can apper only in this literal form, as `xml` prefix cannot be redeclared or unbound
+            let (local, prefix_opt) = name.decompose();
+            if prefix_opt.map_or(false, |prefix| prefix.is_xml()) {
+                decoder.decode_into(&name.into_inner(), key_buf)?;
+            } else {
+                decoder.decode_into(local.into_inner(), key_buf)?;
+            }
         };
 
         Ok(Self {
diff --git a/src/name.rs b/src/name.rs
index 6bed83a..79bfd8b 100644
--- a/src/name.rs
+++ b/src/name.rs
@@ -257,6 +257,18 @@ impl<'a> Prefix<'a> {
     pub const fn into_inner(self) -> &'a [u8] {
         self.0
     }
+
+    /// Checks if this prefix is a special prefix `xml`.
+    #[inline(always)]
+    pub const fn is_xml(&self) -> bool {
+        matches!(self.0, b"xml")
+    }
+
+    /// Checks if this prefix is a special prefix `xmlns`.
+    #[inline(always)]
+    pub const fn is_xmlns(&self) -> bool {
+        matches!(self.0, b"xmlns")
+    }
 }
 impl<'a> Debug for Prefix<'a> {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {


Test patch:
diff --git a/tests/serde-issues.rs b/tests/serde-issues.rs
index f9d4ee1..14c4468 100644
--- a/tests/serde-issues.rs
+++ b/tests/serde-issues.rs
@@ -571,6 +571,27 @@ fn issue683() {
     );
 }
 
+/// Regression test for https://github.com/tafia/quick-xml/issues/841.
+/// `xml:`-attributes should be able to roundtrip serialization - deserialization
+#[test]
+fn issue841() {
+    #[derive(Debug, PartialEq, Deserialize, Serialize)]
+    struct ElementWithXmlLang {
+        #[serde(rename = "$text")]
+        content: String,
+        #[serde(rename = "@xml:lang")]
+        language: String,
+    }
+
+    let value = ElementWithXmlLang {
+        content: "content".to_string(),
+        language: "en-US".to_string(),
+    };
+    let sr = to_string(&value).unwrap();
+    let ds: ElementWithXmlLang = from_str(&sr).unwrap();
+    assert_eq!(ds, value);
+}
+
 /// Regression test for https://github.com/tafia/quick-xml/issues/868.
 #[test]
 fn issue868() {