Repository: tafia/quick-xml (Rust)

Issue:
xml serde read translates optional attributes with value "" to None
If you want to be able to make a difference between attribute missing and attribute empty `""` should serialize to `Some("")` but those attributes deserialize to `None`.
(useful for roundtrips where the output should be equal to input with small change applied)

```rust
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
struct Name {
    #[serde(rename = "@first_name", skip_serializing_if = "Option::is_none")]
    first_name: Option<String>,
    #[serde(rename = "@second_name", skip_serializing_if = "Option::is_none")]
    second_name: Option<String>,
}

#[test]
fn read_and_write() {
    let expected = Name {
        first_name: Some("aaa".to_string()),
        second_name: Some("".to_string()),
    };

    let name =
        quick_xml::de::from_str::<Name>(r#"<Name first_name="aaa" second_name=""/>"#).unwrap();

    assert_eq!(expected, name);

    let s = quick_xml::se::to_string(&name).unwrap();

    assert_eq!(r#"<Name first_name="aaa" second_name=""/>"#, s);
}
```

fails with

```
thread 'read_and_write' panicked at tests/tezt.rs:21:5:
assertion `left == right` failed
  left: Name { first_name: Some("aaa"), second_name: Some("") }
 right: Name { first_name: Some("aaa"), second_name: None }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

I found similar issues that are marked as fixed: #252 and #527


Discussion:
Yes, the problem only on Deserializer side, it is not tested in [tests](https://github.com/tafia/quick-xml/blob/27a93b3259be5ab74387a725386cd9226128ab06/tests/serde-issues.rs#L17-L49) for #252. Probably should be an easy fix.
If you want I can have a look at a PR this evening, however I see you self-assigned this?
I'd looked at this. The current behavior was selected to match `serde_json` behavior which translates `{ "a": null }` to `a: None`. In XML the closest analogues to `null` is empty value of attribute. But maybe we need to review this again.
Json `{ "a": "" }` will probably give you `a: Some("")` which is similar to what this issue is going for?

The tricky part is that for json both `{ "a": null }` and `{}` will return `a: None` so its impossible to do a round trip keeping exact equal representation. For xml there is no such issue.
Great, thanks @Mingun
I see a `0.31.0` release was made but it seems to be missing on crates.io?
https://crates.io/crates/quick-xml



Patch:
diff --git a/Changelog.md b/Changelog.md
index 8de5fa4e..f2eb68b8 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -30,6 +30,8 @@ serde >= 1.0.181
   unit structs, unit variants). `<int>123<something-else/></int>` is no longer valid
   content. Previously all data after `123` up to closing tag would be silently skipped.
 - [#567]: Fixed incorrect deserialization of vectors of enums from sequences of tags.
+- [#671]: Fixed deserialization of empty `simpleType`s (for example, attributes) into
+  `Option` fields: now they are always deserialized as `Some("")`.
 
 ### Misc Changes
 
@@ -58,6 +60,7 @@ serde >= 1.0.181
 [#661]: https://github.com/tafia/quick-xml/pull/661
 [#662]: https://github.com/tafia/quick-xml/pull/662
 [#665]: https://github.com/tafia/quick-xml/pull/665
+[#671]: https://github.com/tafia/quick-xml/issues/671
 
 
 ## 0.30.0 -- 2023-07-23
diff --git a/src/de/simple_type.rs b/src/de/simple_type.rs
index 32d7585d..aa3be1e5 100644
--- a/src/de/simple_type.rs
+++ b/src/de/simple_type.rs
@@ -481,7 +481,32 @@ impl<'de, 'a> SeqAccess<'de> for ListIter<'de, 'a> {
 /// - attribute values (`<... ...="value" ...>`)
 /// - mixed text / CDATA content (`<...>text<![CDATA[cdata]]></...>`)
 ///
+/// This deserializer processes items as following:
+/// - numbers are parsed from a text content using [`FromStr`];
+/// - booleans converted from the text according to the XML [specification]:
+///   - `"true"` and `"1"` converted to `true`;
+///   - `"false"` and `"0"` converted to `false`;
+/// - strings returned as is;
+/// - characters also returned as strings. If string contain more than one character
+///   or empty, it is responsibility of a type to return an error;
+/// - `Option` always deserialized as `Some` using the same deserializer.
+///   If attribute or text content is missed, then the deserializer even wouldn't
+///   be used, so if it is used, then the value should be;
+/// - units (`()`) and unit structs always deserialized successfully;
+/// - newtype structs forwards deserialization to the inner type using the same
+///   deserializer;
+/// - sequences, tuples and tuple structs are deserialized as `xs:list`s. Only
+///   sequences of primitive types is possible to deserialize this way and they
+///   should be delimited by a space (` `, `\t`, `\r`, or `\n`);
+/// - structs and maps returns [`DeError::Unsupported`];
+/// - enums:
+///   - unit variants: just return `()`;
+///   - all other variants returns [`DeError::Unsupported`];
+/// - identifiers are deserialized as strings.
+///
 /// [simple types]: https://www.w3.org/TR/xmlschema11-1/#Simple_Type_Definition
+/// [`FromStr`]: std::str::FromStr
+/// [specification]: https://www.w3.org/TR/xmlschema11-2/#boolean
 pub struct SimpleTypeDeserializer<'de, 'a> {
     /// - In case of attribute contains escaped attribute value
     /// - In case of text contains unescaped text value
@@ -642,11 +667,7 @@ impl<'de, 'a> Deserializer<'de> for SimpleTypeDeserializer<'de, 'a> {
     where
         V: Visitor<'de>,
     {
-        if self.content.is_empty() {
-            visitor.visit_none()
-        } else {
-            visitor.visit_some(self)
-        }
+        visitor.visit_some(self)
     }
 
     fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
@@ -1225,7 +1246,7 @@ mod tests {
         err!(utf8, borrowed_bytes: Bytes = "&lt;escaped&#32;string"
              => Unsupported("binary data content is not supported by XML format"));
 
-        simple!(utf8, option_none: Option<&str> = "" => None);
+        simple!(utf8, option_none: Option<&str> = "" => Some(""));
         simple!(utf8, option_some: Option<&str> = "non-escaped string" => Some("non-escaped string"));
 
         simple_only!(utf8, unit: () = "any data" => ());
@@ -1311,7 +1332,7 @@ mod tests {
         unsupported!(borrowed_bytes: Bytes = "&lt;escaped&#32;string"
                      => "binary data content is not supported by XML format");
 
-        utf16!(option_none: Option<()> = "" => None);
+        utf16!(option_none: Option<()> = "" => Some(()));
         utf16!(option_some: Option<()> = "any data" => Some(()));
 
         utf16!(unit: () = "any data" => ());


Test patch:
diff --git a/tests/serde-issues.rs b/tests/serde-issues.rs
index b1be5963..a3e95b9e 100644
--- a/tests/serde-issues.rs
+++ b/tests/serde-issues.rs
@@ -16,7 +16,7 @@ mod issue252 {
 
     #[test]
     fn attributes() {
-        #[derive(Serialize, Debug, PartialEq)]
+        #[derive(Debug, Deserialize, Serialize, PartialEq)]
         struct OptionalAttributes {
             #[serde(rename = "@a")]
             a: Option<&'static str>,
@@ -26,31 +26,41 @@ mod issue252 {
             b: Option<&'static str>,
         }
 
+        // Writing `a=""` for a `None` we reflects serde_json behavior which also
+        // writes `a: null` for `None`, and reflect they deserialization asymmetry
+        let xml = r#"<OptionalAttributes a=""/>"#;
         assert_eq!(
             to_string(&OptionalAttributes { a: None, b: None }).unwrap(),
-            r#"<OptionalAttributes a=""/>"#
+            xml
         );
         assert_eq!(
-            to_string(&OptionalAttributes {
+            from_str::<OptionalAttributes>(xml).unwrap(),
+            OptionalAttributes {
                 a: Some(""),
-                b: Some("")
-            })
-            .unwrap(),
-            r#"<OptionalAttributes a="" b=""/>"#
-        );
-        assert_eq!(
-            to_string(&OptionalAttributes {
-                a: Some("a"),
-                b: Some("b")
-            })
-            .unwrap(),
-            r#"<OptionalAttributes a="a" b="b"/>"#
+                b: None
+            }
         );
+
+        let value = OptionalAttributes {
+            a: Some(""),
+            b: Some(""),
+        };
+        let xml = r#"<OptionalAttributes a="" b=""/>"#;
+        assert_eq!(to_string(&value).unwrap(), xml);
+        assert_eq!(from_str::<OptionalAttributes>(xml).unwrap(), value);
+
+        let value = OptionalAttributes {
+            a: Some("a"),
+            b: Some("b"),
+        };
+        let xml = r#"<OptionalAttributes a="a" b="b"/>"#;
+        assert_eq!(to_string(&value).unwrap(), xml);
+        assert_eq!(from_str::<OptionalAttributes>(xml).unwrap(), value);
     }
 
     #[test]
     fn elements() {
-        #[derive(Serialize, Debug, PartialEq)]
+        #[derive(Debug, Deserialize, Serialize, PartialEq)]
         struct OptionalElements {
             a: Option<&'static str>,
 
@@ -58,26 +68,36 @@ mod issue252 {
             b: Option<&'static str>,
         }
 
+        // Writing `<a/>` for a `None` we reflects serde_json behavior which also
+        // writes `a: null` for `None`, and reflect they deserialization asymmetry
+        let xml = "<OptionalElements><a/></OptionalElements>";
         assert_eq!(
             to_string(&OptionalElements { a: None, b: None }).unwrap(),
-            r#"<OptionalElements><a/></OptionalElements>"#
+            xml
         );
         assert_eq!(
-            to_string(&OptionalElements {
+            from_str::<OptionalElements>(xml).unwrap(),
+            OptionalElements {
                 a: Some(""),
-                b: Some("")
-            })
-            .unwrap(),
-            r#"<OptionalElements><a/><b/></OptionalElements>"#
-        );
-        assert_eq!(
-            to_string(&OptionalElements {
-                a: Some("a"),
-                b: Some("b")
-            })
-            .unwrap(),
-            r#"<OptionalElements><a>a</a><b>b</b></OptionalElements>"#
+                b: None
+            }
         );
+
+        let value = OptionalElements {
+            a: Some(""),
+            b: Some(""),
+        };
+        let xml = "<OptionalElements><a/><b/></OptionalElements>";
+        assert_eq!(to_string(&value).unwrap(), xml);
+        assert_eq!(from_str::<OptionalElements>(xml).unwrap(), value);
+
+        let value = OptionalElements {
+            a: Some("a"),
+            b: Some("b"),
+        };
+        let xml = "<OptionalElements><a>a</a><b>b</b></OptionalElements>";
+        assert_eq!(to_string(&value).unwrap(), xml);
+        assert_eq!(from_str::<OptionalElements>(xml).unwrap(), value);
     }
 }