Repository: tafia/quick-xml
Pull request #574 (2023-03-06 16:37:49)

Implement correct handling of `$text` fields in enums and add roundtrip tests
This is follow-up PR for #541 in which I forgot to make a corresponding serialization for the deserialization. In order to not make this mistake again, the last 6 commits enables serialization-deserialization roundtrip in serialization tests -- we try to deserialize what was serialized and check that we get the same object.

During that I found that some roundrips does not work, mostly because of serde-rs/serde#1183. That tests does not check the roundtrip.

Some structs failed to derive `Deserialize` due to serde-rs/serde#2371. Although it is possible to implement it manually or use [my fork](https://github.com/Mingun/serde/tree/serde-next) where that error is fixed specially for that, the same tests suffers from the serde-rs/serde#1183 too, so I ended up with the current approach in order to reduce dependencies.


Patch:
diff --git a/Changelog.md b/Changelog.md
index b3103f6..257b41b 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -12,8 +12,8 @@
 
 ### New Features
 
-- [#541]: Deserialize specially named `$text` enum variant in [externally tagged]
-  enums from textual content
+- [#541]: (De)serialize specially named `$text` enum variant in [externally tagged]
+  enums to / from textual content
 - [#556]: `to_writer` and `to_string` now accept `?Sized` types
 - [#556]: Add new `to_writer_with_root` and `to_string_with_root` helper functions
 - [#520]: Add methods `BytesText::inplace_trim_start` and `BytesText::inplace_trim_end`
diff --git a/src/se/content.rs b/src/se/content.rs
index f4f99a0..24c29ac 100644
--- a/src/se/content.rs
+++ b/src/se/content.rs
@@ -1,7 +1,7 @@
 //! Contains serializer for content of an XML element
 
 use crate::errors::serialize::DeError;
-use crate::se::element::{ElementSerializer, Struct};
+use crate::se::element::{ElementSerializer, Struct, Tuple};
 use crate::se::simple_type::{QuoteTarget, SimpleTypeSerializer};
 use crate::se::{Indent, QuoteLevel, XmlName};
 use serde::ser::{
@@ -132,7 +132,7 @@ impl<'w, 'i, W: Write> Serializer for ContentSerializer<'w, 'i, W> {
     type SerializeSeq = Self;
     type SerializeTuple = Self;
     type SerializeTupleStruct = Self;
-    type SerializeTupleVariant = ElementSerializer<'w, 'i, W>;
+    type SerializeTupleVariant = Tuple<'w, 'i, W>;
     type SerializeMap = Impossible<Self::Ok, Self::Error>;
     type SerializeStruct = Impossible<Self::Ok, Self::Error>;
     type SerializeStructVariant = Struct<'w, 'i, W>;
@@ -258,7 +258,7 @@ impl<'w, 'i, W: Write> Serializer for ContentSerializer<'w, 'i, W> {
         // `ElementSerializer::serialize_tuple_variant` is the same as
         // `ElementSerializer::serialize_tuple_struct`, except that it replaces `.key`
         // to `variant` which is not required here
-        ser.serialize_tuple_struct(name, len)
+        ser.serialize_tuple_struct(name, len).map(Tuple::Element)
     }
 
     fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
diff --git a/src/se/element.rs b/src/se/element.rs
index ed6df4f..19a9657 100644
--- a/src/se/element.rs
+++ b/src/se/element.rs
@@ -4,7 +4,7 @@ use crate::de::{TEXT_KEY, VALUE_KEY};
 use crate::errors::serialize::DeError;
 use crate::se::content::ContentSerializer;
 use crate::se::key::QNameSerializer;
-use crate::se::simple_type::{QuoteTarget, SimpleTypeSerializer};
+use crate::se::simple_type::{QuoteTarget, SimpleSeq, SimpleTypeSerializer};
 use crate::se::{Indent, XmlName};
 use serde::ser::{
     Serialize, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple,
@@ -37,7 +37,7 @@ impl<'w, 'k, W: Write> Serializer for ElementSerializer<'w, 'k, W> {
     type SerializeSeq = Self;
     type SerializeTuple = Self;
     type SerializeTupleStruct = Self;
-    type SerializeTupleVariant = Self;
+    type SerializeTupleVariant = Tuple<'w, 'k, W>;
     type SerializeMap = Map<'w, 'k, W>;
     type SerializeStruct = Struct<'w, 'k, W>;
     type SerializeStructVariant = Struct<'w, 'k, W>;
@@ -105,12 +105,19 @@ impl<'w, 'k, W: Write> Serializer for ElementSerializer<'w, 'k, W> {
 
     fn serialize_unit_variant(
         self,
-        _name: &'static str,
+        name: &'static str,
         _variant_index: u32,
         variant: &'static str,
     ) -> Result<Self::Ok, Self::Error> {
-        let name = XmlName::try_from(variant)?;
-        self.ser.write_empty(name)
+        if variant == TEXT_KEY {
+            // We should write some text but we don't known what text to write
+            Err(DeError::Unsupported(
+                format!("`{}::$text` unit variant cannot be serialized", name).into(),
+            ))
+        } else {
+            let name = XmlName::try_from(variant)?;
+            self.ser.write_empty(name)
+        }
     }
 
     fn serialize_newtype_struct<T: ?Sized + Serialize>(
@@ -128,8 +135,13 @@ impl<'w, 'k, W: Write> Serializer for ElementSerializer<'w, 'k, W> {
         variant: &'static str,
         value: &T,
     ) -> Result<Self::Ok, Self::Error> {
-        self.key = XmlName::try_from(variant)?;
-        value.serialize(self)
+        if variant == TEXT_KEY {
+            value.serialize(self.ser.into_simple_type_serializer())?;
+            Ok(())
+        } else {
+            self.key = XmlName::try_from(variant)?;
+            value.serialize(self)
+        }
     }
 
     #[inline]
@@ -159,8 +171,15 @@ impl<'w, 'k, W: Write> Serializer for ElementSerializer<'w, 'k, W> {
         variant: &'static str,
         len: usize,
     ) -> Result<Self::SerializeTupleVariant, Self::Error> {
-        self.key = XmlName::try_from(variant)?;
-        self.serialize_tuple_struct(name, len)
+        if variant == TEXT_KEY {
+            self.ser
+                .into_simple_type_serializer()
+                .serialize_tuple_struct(name, len)
+                .map(Tuple::Text)
+        } else {
+            self.key = XmlName::try_from(variant)?;
+            self.serialize_tuple_struct(name, len).map(Tuple::Element)
+        }
     }
 
     fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
@@ -195,8 +214,14 @@ impl<'w, 'k, W: Write> Serializer for ElementSerializer<'w, 'k, W> {
         variant: &'static str,
         len: usize,
     ) -> Result<Self::SerializeStructVariant, Self::Error> {
-        self.key = XmlName::try_from(variant)?;
-        self.serialize_struct(name, len)
+        if variant == TEXT_KEY {
+            Err(DeError::Unsupported(
+                format!("`{}::$text` struct variant cannot be serialized", name).into(),
+            ))
+        } else {
+            self.key = XmlName::try_from(variant)?;
+            self.serialize_struct(name, len)
+        }
     }
 }
 
@@ -259,7 +284,19 @@ impl<'w, 'k, W: Write> SerializeTupleStruct for ElementSerializer<'w, 'k, W> {
     }
 }
 
-impl<'w, 'k, W: Write> SerializeTupleVariant for ElementSerializer<'w, 'k, W> {
+////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/// A serializer for tuple variants. Tuples can be serialized in two modes:
+/// - wrapping each tuple field into a tag
+/// - without wrapping, fields are delimited by a space
+pub enum Tuple<'w, 'k, W: Write> {
+    /// Serialize each tuple field as an element
+    Element(ElementSerializer<'w, 'k, W>),
+    /// Serialize tuple as an `xs:list`: space-delimited content of fields
+    Text(SimpleSeq<'k, &'w mut W>),
+}
+
+impl<'w, 'k, W: Write> SerializeTupleVariant for Tuple<'w, 'k, W> {
     type Ok = ();
     type Error = DeError;
 
@@ -268,12 +305,18 @@ impl<'w, 'k, W: Write> SerializeTupleVariant for ElementSerializer<'w, 'k, W> {
     where
         T: ?Sized + Serialize,
     {
-        <Self as SerializeTuple>::serialize_element(self, value)
+        match self {
+            Tuple::Element(ser) => SerializeTuple::serialize_element(ser, value),
+            Tuple::Text(ser) => SerializeTuple::serialize_element(ser, value),
+        }
     }
 
     #[inline]
     fn end(self) -> Result<Self::Ok, Self::Error> {
-        <Self as SerializeTuple>::end(self)
+        match self {
+            Tuple::Element(ser) => SerializeTuple::end(ser),
+            Tuple::Text(ser) => SerializeTuple::end(ser).map(|_| ()),
+        }
     }
 }
 


Test patch:
diff --git a/tests/serde-se.rs b/tests/serde-se.rs
index 0553f41..5e1a2a7 100644
--- a/tests/serde-se.rs
+++ b/tests/serde-se.rs
@@ -1,54 +1,55 @@
+use quick_xml::de::from_str;
 use quick_xml::se::Serializer;
 use quick_xml::utils::Bytes;
 use quick_xml::DeError;
 
-use serde::{serde_if_integer128, Serialize};
+use serde::{serde_if_integer128, Deserialize, Serialize};
 use std::collections::BTreeMap;
 
-#[derive(Serialize)]
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
 struct Unit;
 
-#[derive(Serialize)]
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
 struct Newtype(bool);
 
-#[derive(Serialize)]
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
 struct Tuple(f32, &'static str);
 
-#[derive(Serialize)]
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
 struct Struct {
     float: f64,
     string: &'static str,
 }
 
-#[derive(Serialize)]
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
 struct NestedStruct {
     nested: Nested,
     string: &'static str,
 }
 
-#[derive(Serialize)]
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
 struct FlattenStruct {
     #[serde(flatten)]
     nested: Nested,
     string: &'static str,
 }
 
-#[derive(Serialize)]
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
 struct Nested {
     float: f64,
 }
 
-#[derive(Serialize)]
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
 struct Empty {}
 
-#[derive(Serialize)]
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
 struct Text {
     #[serde(rename = "$text")]
     float: f64,
     string: &'static str,
 }
 
-#[derive(Serialize)]
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
 enum ExternallyTagged {
     Unit,
     Newtype(bool),
@@ -61,20 +62,32 @@ enum ExternallyTagged {
         nested: Nested,
         string: &'static str,
     },
-    Flatten {
-        #[serde(flatten)]
-        nested: Nested,
-        string: &'static str,
-    },
-    Empty {},
+    /// `float` field serialized as textual content instead of a tag
     Text {
         #[serde(rename = "$text")]
         float: f64,
         string: &'static str,
     },
+    Empty {},
 }
 
+/// Having both `#[serde(flatten)]` and `'static` fields in one struct leads to
+/// incorrect code generation when deriving `Deserialize`.
+///
+/// TODO: Merge into main enum after fixing <https://github.com/serde-rs/serde/issues/2371>
+///
+/// Anyway, deserialization of that type in roundtrip suffers from
+/// <https://github.com/serde-rs/serde/issues/1183>
 #[derive(Serialize)]
+enum ExternallyTaggedWorkaround {
+    Flatten {
+        #[serde(flatten)]
+        nested: Nested,
+        string: &'static str,
+    },
+}
+
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
 #[serde(tag = "tag")]
 enum InternallyTagged {
     Unit,
@@ -89,20 +102,33 @@ enum InternallyTagged {
         nested: Nested,
         string: &'static str,
     },
-    Flatten {
-        #[serde(flatten)]
-        nested: Nested,
-        string: &'static str,
-    },
-    Empty {},
+    /// `float` field serialized as textual content instead of a tag
     Text {
         #[serde(rename = "$text")]
         float: f64,
         string: &'static str,
     },
+    Empty {},
 }
 
-#[derive(Serialize)]
+/// Having both `#[serde(flatten)]` and `'static` fields in one struct leads to
+/// incorrect code generation when deriving `Deserialize`.
+///
+/// TODO: Merge into main enum after fixing <https://github.com/serde-rs/serde/issues/2371>
+///
+/// Anyway, deserialization of that type in roundtrip suffers from
+/// <https://github.com/serde-rs/serde/issues/1183>
+#[derive(Debug, PartialEq, Serialize)]
+#[serde(tag = "tag")]
+enum InternallyTaggedWorkaround {
+    Flatten {
+        #[serde(flatten)]
+        nested: Nested,
+        string: &'static str,
+    },
+}
+
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
 #[serde(tag = "tag", content = "content")]
 enum AdjacentlyTagged {
     Unit,
@@ -116,20 +142,33 @@ enum AdjacentlyTagged {
         nested: Nested,
         string: &'static str,
     },
-    Flatten {
-        #[serde(flatten)]
-        nested: Nested,
-        string: &'static str,
-    },
-    Empty {},
+    /// `float` field serialized as textual content instead of a tag
     Text {
         #[serde(rename = "$text")]
         float: f64,
         string: &'static str,
     },
+    Empty {},
 }
 
+/// Having both `#[serde(flatten)]` and `'static` fields in one struct leads to
+/// incorrect code generation when deriving `Deserialize`.
+///
+/// TODO: Merge into main enum after fixing <https://github.com/serde-rs/serde/issues/2371>
+///
+/// Anyway, deserialization of that type in roundtrip suffers from
+/// <https://github.com/serde-rs/serde/issues/1183>
 #[derive(Serialize)]
+#[serde(tag = "tag", content = "content")]
+enum AdjacentlyTaggedWorkaround {
+    Flatten {
+        #[serde(flatten)]
+        nested: Nested,
+        string: &'static str,
+    },
+}
+
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
 #[serde(untagged)]
 enum Untagged {
     Unit,
@@ -143,17 +182,30 @@ enum Untagged {
         nested: Nested,
         string: &'static str,
     },
-    Flatten {
-        #[serde(flatten)]
-        nested: Nested,
-        string: &'static str,
-    },
-    Empty {},
+    /// `float` field serialized as textual content instead of a tag
     Text {
         #[serde(rename = "$text")]
         float: f64,
         string: &'static str,
     },
+    Empty {},
+}
+
+/// Having both `#[serde(flatten)]` and `'static` fields in one struct leads to
+/// incorrect code generation when deriving `Deserialize`.
+///
+/// TODO: Merge into main enum after fixing <https://github.com/serde-rs/serde/issues/2371>
+///
+/// Anyway, deserialization of that type in roundtrip suffers from
+/// <https://github.com/serde-rs/serde/issues/1183>
+#[derive(Serialize)]
+#[serde(untagged)]
+enum UntaggedWorkaround {
+    Flatten {
+        #[serde(flatten)]
+        nested: Nested,
+        string: &'static str,
+    },
 }
 
 mod without_root {
@@ -164,11 +216,29 @@ mod without_root {
         ($name:ident: $data:expr => $expected:literal) => {
             #[test]
             fn $name() {
-                let mut buffer = String::new();
-                let ser = Serializer::new(&mut buffer);
+                serialize_as!(@ $data => $expected);
+
+                // Roundtrip to ensure that serializer corresponds to deserializer
+                assert_eq!(
+                    $data,
+                    from_str($expected).expect("deserialization roundtrip"),
+                    "deserialization roundtrip",
+                );
+            }
+        };
+        (@ $data:expr => $expected:literal) => {
+            let mut buffer = String::new();
+            let ser = Serializer::new(&mut buffer);
 
-                $data.serialize(ser).unwrap();
-                assert_eq!(buffer, $expected);
+            $data.serialize(ser).unwrap();
+            assert_eq!(buffer, $expected);
+        };
+    }
+    macro_rules! serialize_as_only {
+        ($name:ident: $data:expr => $expected:literal) => {
+            #[test]
+            fn $name() {
+                serialize_as!(@ $data => $expected);
             }
         };
     }
@@ -330,8 +400,10 @@ mod without_root {
                         </nested>\
                         <string>answer</string>\
                     </Holder>");
-            serialize_as!(flatten_struct:
-                ExternallyTagged::Flatten {
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(flatten_struct:
+                ExternallyTaggedWorkaround::Flatten {
                     nested: Nested { float: 42.0 },
                     string: "answer",
                 }
@@ -351,8 +423,54 @@ mod without_root {
                         42\
                         <string>answer</string>\
                     </Text>");
+
+            /// Test serialization of the specially named variant `$text`
+            mod text {
+                use super::*;
+                use pretty_assertions::assert_eq;
+
+                #[derive(Debug, PartialEq, Deserialize, Serialize)]
+                enum Unit {
+                    #[serde(rename = "$text")]
+                    Text,
+                }
+                #[derive(Debug, PartialEq, Deserialize, Serialize)]
+                enum Newtype<'a> {
+                    #[serde(rename = "$text")]
+                    Text(&'a str),
+                }
+                #[derive(Debug, PartialEq, Deserialize, Serialize)]
+                enum Tuple {
+                    #[serde(rename = "$text")]
+                    Text(f64, String),
+                }
+                #[derive(Debug, PartialEq, Deserialize, Serialize)]
+                enum Struct<'a> {
+                    #[serde(rename = "$text")]
+                    Text { float: f64, string: &'a str },
+                }
+
+                // It is unknown how to exactly serialize unit to a text
+                err!(unit: Unit::Text => Unsupported("`Unit::$text` unit variant cannot be serialized"));
+                serialize_as!(newtype: Newtype::Text("newtype text") => "newtype text");
+                // Tuple variant serialized as an `xs:list`
+                serialize_as!(tuple: Tuple::Text(4.2, "newtype-text".into()) => "4.2 newtype-text");
+                // Note, that spaces in strings, even escaped, would represent
+                // the list item delimiters. Non-symmetric serialization follows
+                // tradition: the XmlBeans Java library have the same behavior.
+                // See also <https://stackoverflow.com/questions/45494204/escape-space-in-xml-xslist>
+                serialize_as_only!(tuple_with_spaces: Tuple::Text(4.2, "newtype text".into()) => "4.2 newtype&#32;text");
+                // Struct variant cannot be directly serialized to a text
+                err!(struct_:
+                    Struct::Text {
+                        float: 4.2,
+                        string: "newtype text",
+                    }
+                    => Unsupported("`Struct::$text` struct variant cannot be serialized"));
+            }
         }
 
+        /// Name `$text` has no special meaning in internally tagged enums
         mod internally_tagged {
             use super::*;
             use pretty_assertions::assert_eq;
@@ -364,13 +482,17 @@ mod without_root {
                     </InternallyTagged>");
             // serde serializes internally tagged newtype structs by delegating
             // serialization to the inner type and augmenting it with a tag
-            serialize_as!(newtype:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(newtype:
                 InternallyTagged::Newtype(Nested { float: 4.2 })
                 => "<Nested>\
                         <tag>Newtype</tag>\
                         <float>4.2</float>\
                     </Nested>");
-            serialize_as!(struct_:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(struct_:
                 InternallyTagged::Struct {
                     float: 42.0,
                     string: "answer"
@@ -380,7 +502,9 @@ mod without_root {
                         <float>42</float>\
                         <string>answer</string>\
                     </InternallyTagged>");
-            serialize_as!(nested_struct:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(nested_struct:
                 InternallyTagged::Holder {
                     nested: Nested { float: 42.0 },
                     string: "answer",
@@ -395,7 +519,7 @@ mod without_root {
             // serde serializes flatten structs as maps, and we do not support
             // serialization of maps without root tag
             err!(flatten_struct:
-                InternallyTagged::Flatten {
+                InternallyTaggedWorkaround::Flatten {
                     nested: Nested { float: 42.0 },
                     string: "answer",
                 }
@@ -405,7 +529,9 @@ mod without_root {
                 => "<InternallyTagged>\
                         <tag>Empty</tag>\
                     </InternallyTagged>");
-            serialize_as!(text:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(text:
                 InternallyTagged::Text {
                     float: 42.0,
                     string: "answer"
@@ -417,6 +543,7 @@ mod without_root {
                     </InternallyTagged>");
         }
 
+        /// Name `$text` has no special meaning in adjacently tagged enums
         mod adjacently_tagged {
             use super::*;
             use pretty_assertions::assert_eq;
@@ -465,18 +592,20 @@ mod without_root {
                             <string>answer</string>\
                         </content>\
                     </AdjacentlyTagged>");
-            serialize_as!(flatten_struct:
-                AdjacentlyTagged::Flatten {
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(flatten_struct:
+                AdjacentlyTaggedWorkaround::Flatten {
                     nested: Nested { float: 42.0 },
                     string: "answer",
                 }
-                => "<AdjacentlyTagged>\
+                => "<AdjacentlyTaggedWorkaround>\
                         <tag>Flatten</tag>\
                         <content>\
                             <float>42</float>\
                             <string>answer</string>\
                         </content>\
-                    </AdjacentlyTagged>");
+                    </AdjacentlyTaggedWorkaround>");
             serialize_as!(empty_struct:
                 AdjacentlyTagged::Empty {}
                 => "<AdjacentlyTagged>\
@@ -497,6 +626,7 @@ mod without_root {
                     </AdjacentlyTagged>");
         }
 
+        /// Name `$text` has no special meaning in untagged enums
         mod untagged {
             use super::*;
             use pretty_assertions::assert_eq;
@@ -509,7 +639,9 @@ mod without_root {
                 => Unsupported("cannot serialize `bool` without defined root tag"));
             err!(tuple_struct: Untagged::Tuple(42.0, "answer")
                 => Unsupported("cannot serialize unnamed tuple without defined root tag"));
-            serialize_as!(struct_:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(struct_:
                 Untagged::Struct {
                     float: 42.0,
                     string: "answer",
@@ -518,7 +650,9 @@ mod without_root {
                         <float>42</float>\
                         <string>answer</string>\
                     </Untagged>");
-            serialize_as!(nested_struct:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(nested_struct:
                 Untagged::Holder {
                     nested: Nested { float: 42.0 },
                     string: "answer",
@@ -529,8 +663,10 @@ mod without_root {
                         </nested>\
                         <string>answer</string>\
                     </Untagged>");
+            // serde serializes flatten structs as maps, and we do not support
+            // serialization of maps without root tag
             err!(flatten_struct:
-                Untagged::Flatten {
+                UntaggedWorkaround::Flatten {
                     nested: Nested { float: 42.0 },
                     string: "answer",
                 }
@@ -538,7 +674,9 @@ mod without_root {
             serialize_as!(empty_struct:
                 Untagged::Empty {}
                 => "<Untagged/>");
-            serialize_as!(text:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(text:
                 Untagged::Text {
                     float: 42.0,
                     string: "answer"
@@ -550,6 +688,7 @@ mod without_root {
         }
     }
 
+    /// Do not run roundtrip in those tests because the results the same as without indentation
     mod with_indent {
         use super::*;
         use pretty_assertions::assert_eq;
@@ -725,7 +864,7 @@ mod without_root {
                             <string>answer</string>\n\
                         </Holder>");
                 serialize_as!(flatten_struct:
-                    ExternallyTagged::Flatten {
+                    ExternallyTaggedWorkaround::Flatten {
                         nested: Nested { float: 42.0 },
                         string: "answer",
                     }
@@ -745,8 +884,49 @@ mod without_root {
                             42\n  \
                             <string>answer</string>\n\
                         </Text>");
+
+                /// Test serialization of the specially named variant `$text`
+                mod text {
+                    use super::*;
+                    use pretty_assertions::assert_eq;
+
+                    #[derive(Debug, PartialEq, Deserialize, Serialize)]
+                    enum Unit {
+                        #[serde(rename = "$text")]
+                        Text,
+                    }
+                    #[derive(Debug, PartialEq, Deserialize, Serialize)]
+                    enum Newtype<'a> {
+                        #[serde(rename = "$text")]
+                        Text(&'a str),
+                    }
+                    #[derive(Debug, PartialEq, Deserialize, Serialize)]
+                    enum Tuple<'a> {
+                        #[serde(rename = "$text")]
+                        Text(f64, &'a str),
+                    }
+                    #[derive(Debug, PartialEq, Deserialize, Serialize)]
+                    enum Struct<'a> {
+                        #[serde(rename = "$text")]
+                        Text { float: f64, string: &'a str },
+                    }
+
+                    // It is unknown how to exactly serialize unit to a text
+                    err!(unit: Unit::Text => Unsupported("`Unit::$text` unit variant cannot be serialized"));
+                    serialize_as!(newtype: Newtype::Text("newtype text") => "newtype text");
+                    // Tuple variant serialized as an `xs:list`
+                    serialize_as!(tuple: Tuple::Text(4.2, "newtype text") => "4.2 newtype&#32;text");
+                    // Struct variant cannot be directly serialized to a text
+                    err!(struct_:
+                        Struct::Text {
+                            float: 4.2,
+                            string: "newtype text",
+                        }
+                        => Unsupported("`Struct::$text` struct variant cannot be serialized"));
+                }
             }
 
+            /// Name `$text` has no special meaning in untagged enums
             mod internally_tagged {
                 use super::*;
                 use pretty_assertions::assert_eq;
@@ -789,7 +969,7 @@ mod without_root {
                 // serde serializes flatten structs as maps, and we do not support
                 // serialization of maps without root tag
                 err!(flatten_struct:
-                    InternallyTagged::Flatten {
+                    InternallyTaggedWorkaround::Flatten {
                         nested: Nested { float: 42.0 },
                         string: "answer",
                     }
@@ -811,6 +991,7 @@ mod without_root {
                         </InternallyTagged>");
             }
 
+            /// Name `$text` has no special meaning in untagged enums
             mod adjacently_tagged {
                 use super::*;
                 use pretty_assertions::assert_eq;
@@ -860,17 +1041,17 @@ mod without_root {
                             </content>\n\
                         </AdjacentlyTagged>");
                 serialize_as!(flatten_struct:
-                    AdjacentlyTagged::Flatten {
+                    AdjacentlyTaggedWorkaround::Flatten {
                         nested: Nested { float: 42.0 },
                         string: "answer",
                     }
-                    => "<AdjacentlyTagged>\n  \
+                    => "<AdjacentlyTaggedWorkaround>\n  \
                             <tag>Flatten</tag>\n  \
                             <content>\n    \
                                 <float>42</float>\n    \
                                 <string>answer</string>\n  \
                             </content>\n\
-                        </AdjacentlyTagged>");
+                        </AdjacentlyTaggedWorkaround>");
                 serialize_as!(empty_struct:
                     AdjacentlyTagged::Empty {}
                     => "<AdjacentlyTagged>\n  \
@@ -891,6 +1072,7 @@ mod without_root {
                         </AdjacentlyTagged>");
             }
 
+            /// Name `$text` has no special meaning in untagged enums
             mod untagged {
                 use super::*;
                 use pretty_assertions::assert_eq;
@@ -922,7 +1104,7 @@ mod without_root {
                             <string>answer</string>\n\
                         </Untagged>");
                 err!(flatten_struct:
-                    Untagged::Flatten {
+                    UntaggedWorkaround::Flatten {
                         nested: Nested { float: 42.0 },
                         string: "answer",
                     }
@@ -952,11 +1134,42 @@ mod with_root {
         ($name:ident: $data:expr => $expected:literal) => {
             #[test]
             fn $name() {
-                let mut buffer = String::new();
-                let ser = Serializer::with_root(&mut buffer, Some("root")).unwrap();
+                serialize_as!(@ $data => $expected);
+
+                // Roundtrip to ensure that serializer corresponds to deserializer
+                assert_eq!(
+                    $data,
+                    from_str($expected).expect("deserialization roundtrip"),
+                    "deserialization roundtrip",
+                );
+            }
+        };
+        ($name:ident: $data:expr ; $ty:ty => $expected:literal) => {
+            #[test]
+            fn $name() {
+                serialize_as!(@ $data => $expected);
+
+                // Roundtrip to ensure that serializer corresponds to deserializer
+                assert_eq!(
+                    $data,
+                    from_str::<'_, $ty>($expected).expect("deserialization roundtrip"),
+                    "deserialization roundtrip",
+                );
+            }
+        };
+        (@ $data:expr => $expected:literal) => {
+            let mut buffer = String::new();
+            let ser = Serializer::with_root(&mut buffer, Some("root")).unwrap();
 
-                $data.serialize(ser).unwrap();
-                assert_eq!(buffer, $expected);
+            $data.serialize(ser).unwrap();
+            assert_eq!(buffer, $expected);
+        };
+    }
+    macro_rules! serialize_as_only {
+        ($name:ident: $data:expr => $expected:literal) => {
+            #[test]
+            fn $name() {
+                serialize_as!(@ $data => $expected);
             }
         };
     }
@@ -1014,10 +1227,11 @@ mod with_root {
     serialize_as!(char_amp:         '&'  => "<root>&amp;</root>");
     serialize_as!(char_apos:        '\'' => "<root>&apos;</root>");
     serialize_as!(char_quot:        '"'  => "<root>&quot;</root>");
-    serialize_as!(char_space:       ' '  => "<root> </root>");
+    // FIXME: Probably we should trim only for specified types when deserialize
+    serialize_as_only!(char_space:       ' '  => "<root> </root>");
 
-    serialize_as!(str_non_escaped: "non-escaped string" => "<root>non-escaped string</root>");
-    serialize_as!(str_escaped:  "<\"escaped & string'>" => "<root>&lt;&quot;escaped &amp; string&apos;&gt;</root>");
+    serialize_as!(str_non_escaped: "non-escaped string"; &str => "<root>non-escaped string</root>");
+    serialize_as!(str_escaped: "<\"escaped & string'>"; String => "<root>&lt;&quot;escaped &amp; string&apos;&gt;</root>");
 
     err!(bytes: Bytes(b"<\"escaped & bytes'>") => Unsupported("`serialize_bytes` not supported yet"));
 
@@ -1036,12 +1250,13 @@ mod with_root {
         => "<root>true</root>");
 
     serialize_as!(seq:
-        vec![1, 2, 3]
+        vec![1, 2, 3]; Vec<usize>
         => "<root>1</root>\
             <root>2</root>\
             <root>3</root>");
     serialize_as!(tuple:
-        ("<\"&'>", "with\t\r\n spaces", 3usize)
+        // Use to_string() to get owned type that is required for deserialization
+        ("<\"&'>".to_string(), "with\t\r\n spaces", 3usize)
         => "<root>&lt;&quot;&amp;&apos;&gt;</root>\
             <root>with\t\r\n spaces</root>\
             <root>3</root>");
@@ -1076,7 +1291,9 @@ mod with_root {
                 </nested>\
                 <string>answer</string>\
             </root>");
-    serialize_as!(flatten_struct:
+    // NOTE: Cannot be deserialized in roundtrip due to
+    // https://github.com/serde-rs/serde/issues/1183
+    serialize_as_only!(flatten_struct:
         FlattenStruct {
             nested: Nested { float: 42.0 },
             string: "answer",
@@ -1135,8 +1352,10 @@ mod with_root {
                         </nested>\
                         <string>answer</string>\
                     </Holder>");
-            serialize_as!(flatten_struct:
-                ExternallyTagged::Flatten {
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(flatten_struct:
+                ExternallyTaggedWorkaround::Flatten {
                     nested: Nested { float: 42.0 },
                     string: "answer",
                 }
@@ -1156,8 +1375,54 @@ mod with_root {
                         42\
                         <string>answer</string>\
                     </Text>");
+
+            /// Test serialization of the specially named variant `$text`
+            mod text {
+                use super::*;
+                use pretty_assertions::assert_eq;
+
+                #[derive(Debug, PartialEq, Deserialize, Serialize)]
+                enum Unit {
+                    #[serde(rename = "$text")]
+                    Text,
+                }
+                #[derive(Debug, PartialEq, Deserialize, Serialize)]
+                enum Newtype<'a> {
+                    #[serde(rename = "$text")]
+                    Text(&'a str),
+                }
+                #[derive(Debug, PartialEq, Deserialize, Serialize)]
+                enum Tuple {
+                    #[serde(rename = "$text")]
+                    Text(f64, String),
+                }
+                #[derive(Debug, PartialEq, Deserialize, Serialize)]
+                enum Struct<'a> {
+                    #[serde(rename = "$text")]
+                    Text { float: f64, string: &'a str },
+                }
+
+                // It is unknown how to exactly serialize unit to a text
+                err!(unit: Unit::Text => Unsupported("`Unit::$text` unit variant cannot be serialized"));
+                serialize_as!(newtype: Newtype::Text("newtype text") => "newtype text");
+                // Tuple variant serialized as an `xs:list`
+                serialize_as!(tuple: Tuple::Text(4.2, "newtype-text".into()) => "4.2 newtype-text");
+                // Note, that spaces in strings, even escaped, would represent
+                // the list item delimiters. Non-symmetric serialization follows
+                // tradition: the XmlBeans Java library have the same behavior.
+                // See also <https://stackoverflow.com/questions/45494204/escape-space-in-xml-xslist>
+                serialize_as_only!(tuple_with_spaces: Tuple::Text(4.2, "newtype text".into()) => "4.2 newtype&#32;text");
+                // Struct variant cannot be directly serialized to a text
+                err!(struct_:
+                    Struct::Text {
+                        float: 4.2,
+                        string: "newtype text",
+                    }
+                    => Unsupported("`Struct::$text` struct variant cannot be serialized"));
+            }
         }
 
+        /// Name `$text` has no special meaning in adjacently tagged enums
         mod internally_tagged {
             use super::*;
             use pretty_assertions::assert_eq;
@@ -1167,13 +1432,17 @@ mod with_root {
                 => "<root>\
                         <tag>Unit</tag>\
                     </root>");
-            serialize_as!(newtype:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(newtype:
                 InternallyTagged::Newtype(Nested { float: 4.2 })
                 => "<root>\
                         <tag>Newtype</tag>\
                         <float>4.2</float>\
                     </root>");
-            serialize_as!(struct_:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(struct_:
                 InternallyTagged::Struct {
                     float: 42.0,
                     string: "answer",
@@ -1183,7 +1452,9 @@ mod with_root {
                         <float>42</float>\
                         <string>answer</string>\
                     </root>");
-            serialize_as!(nested_struct:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(nested_struct:
                 InternallyTagged::Holder {
                     nested: Nested { float: 42.0 },
                     string: "answer",
@@ -1195,8 +1466,10 @@ mod with_root {
                         </nested>\
                         <string>answer</string>\
                     </root>");
-            serialize_as!(flatten_struct:
-                InternallyTagged::Flatten {
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(flatten_struct:
+                InternallyTaggedWorkaround::Flatten {
                     nested: Nested { float: 42.0 },
                     string: "answer",
                 }
@@ -1210,7 +1483,9 @@ mod with_root {
                 => "<root>\
                         <tag>Empty</tag>\
                     </root>");
-            serialize_as!(text:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(text:
                 InternallyTagged::Text {
                     float: 42.0,
                     string: "answer"
@@ -1222,6 +1497,7 @@ mod with_root {
                     </root>");
         }
 
+        /// Name `$text` has no special meaning in adjacently tagged enums
         mod adjacently_tagged {
             use super::*;
             use pretty_assertions::assert_eq;
@@ -1270,8 +1546,10 @@ mod with_root {
                             <string>answer</string>\
                         </content>\
                     </root>");
-            serialize_as!(flatten_struct:
-                AdjacentlyTagged::Flatten {
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(flatten_struct:
+                AdjacentlyTaggedWorkaround::Flatten {
                     nested: Nested { float: 42.0 },
                     string: "answer",
                 }
@@ -1302,21 +1580,30 @@ mod with_root {
                     </root>");
         }
 
+        /// Name `$text` has no special meaning in untagged enums
         mod untagged {
             use super::*;
             use pretty_assertions::assert_eq;
 
-            serialize_as!(unit:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(unit:
                 Untagged::Unit
                 => "<root/>");
-            serialize_as!(newtype:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(newtype:
                 Untagged::Newtype(true)
                 => "<root>true</root>");
-            serialize_as!(tuple_struct:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(tuple_struct:
                 Untagged::Tuple(42.0, "answer")
                 => "<root>42</root>\
                     <root>answer</root>");
-            serialize_as!(struct_:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(struct_:
                 Untagged::Struct {
                     float: 42.0,
                     string: "answer",
@@ -1325,7 +1612,9 @@ mod with_root {
                         <float>42</float>\
                         <string>answer</string>\
                     </root>");
-            serialize_as!(nested_struct:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(nested_struct:
                 Untagged::Holder {
                     nested: Nested { float: 42.0 },
                     string: "answer",
@@ -1336,8 +1625,10 @@ mod with_root {
                         </nested>\
                         <string>answer</string>\
                     </root>");
-            serialize_as!(flatten_struct:
-                Untagged::Flatten {
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(flatten_struct:
+                UntaggedWorkaround::Flatten {
                     nested: Nested { float: 42.0 },
                     string: "answer",
                 }
@@ -1348,7 +1639,9 @@ mod with_root {
             serialize_as!(empty_struct:
                 Untagged::Empty {}
                 => "<root/>");
-            serialize_as!(text:
+            // NOTE: Cannot be deserialized in roundtrip due to
+            // https://github.com/serde-rs/serde/issues/1183
+            serialize_as_only!(text:
                 Untagged::Text {
                     float: 42.0,
                     string: "answer"