From ff71e9ee4b43abe5439dcc81e0f0d3ebf5dddcd6 Mon Sep 17 00:00:00 2001 From: Jonas Dittrich Date: Thu, 23 Oct 2025 01:09:26 +0200 Subject: [PATCH 1/4] Fix serialization of structs with only text content --- src/se/simple_type.rs | 50 ++++++++++++++++++++++++++++++++++++++----- tests/serde-se.rs | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/se/simple_type.rs b/src/se/simple_type.rs index 7ca621b8..d25ac345 100644 --- a/src/se/simple_type.rs +++ b/src/se/simple_type.rs @@ -3,10 +3,12 @@ //! [simple types]: https://www.w3schools.com/xml/el_simpletype.asp //! [as defined]: https://www.w3.org/TR/xmlschema11-1/#Simple_Type_Definition +use crate::de::TEXT_KEY; use crate::escape::_escape; +use crate::se::text::TextSerializer; use crate::se::{QuoteLevel, SeError}; use serde::ser::{ - Impossible, Serialize, SerializeSeq, SerializeTuple, SerializeTupleStruct, + Impossible, Serialize, SerializeSeq, SerializeStruct, SerializeTuple, SerializeTupleStruct, SerializeTupleVariant, Serializer, }; use serde::serde_if_integer128; @@ -411,6 +413,34 @@ impl SimpleTypeSerializer { Ok(self.writer.write_str(value)?) } } +impl<'w, W: Write> SerializeStruct for SimpleSeq { + type Ok = W; + type Error = SeError; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> { + if key == TEXT_KEY { + let ser = TextSerializer(SimpleTypeSerializer { + writer: &mut self.writer, + target: self.target, + level: self.level, + }); + value.serialize(ser)?; + Ok(()) + } else { + Err(SeError::Unsupported( + format!("only `{TEXT_KEY}` field is supported in text content").into(), + )) + } + } + + fn end(self) -> Result { + SerializeSeq::end(self) + } +} impl Serializer for SimpleTypeSerializer { type Ok = W; @@ -421,7 +451,7 @@ impl Serializer for SimpleTypeSerializer { type SerializeTupleStruct = SimpleSeq; type SerializeTupleVariant = Impossible; type SerializeMap = Impossible; - type SerializeStruct = Impossible; + type SerializeStruct = SimpleSeq; type SerializeStructVariant = Impossible; write_primitive!(); @@ -499,14 +529,24 @@ impl Serializer for SimpleTypeSerializer { )) } + #[inline] fn serialize_struct( self, name: &'static str, - _len: usize, + len: usize, ) -> Result { + if len == 1 { + let seq = SimpleSeq { + writer: self.writer, + target: self.target, + level: self.level, + is_empty: true, + }; + return Ok(seq); + } Err(SeError::Unsupported( format!( - "cannot serialize struct `{}` as an attribute or text content value", + "cannot serialize struct `{}` having more than one field as an attribute or text content value", name ) .into(), @@ -1128,7 +1168,7 @@ mod tests { err!(map: BTreeMap::from([(1, 2), (3, 4)]) => Unsupported("cannot serialize map as an attribute or text content value")); err!(struct_: Struct { key: "answer", val: 42 } - => Unsupported("cannot serialize struct `Struct` as an attribute or text content value")); + => Unsupported("cannot serialize struct `Struct` having more than one field as an attribute or text content value")); err!(enum_struct: Enum::Struct { key: "answer", val: 42 } => Unsupported("cannot serialize enum struct variant `Enum::Struct` as an attribute or text content value")); } diff --git a/tests/serde-se.rs b/tests/serde-se.rs index 051e47c9..2f85b851 100644 --- a/tests/serde-se.rs +++ b/tests/serde-se.rs @@ -2295,4 +2295,46 @@ mod with_root { "); } } + + mod list { + use quick_xml::se::to_string_with_root; + use serde_derive::Serialize; + + #[test] + fn issue906() { + #[derive(Debug, PartialEq, Eq, Serialize)] + pub struct FooType { + #[serde(rename = "a-list")] + pub a_list: ListType, + } + + #[derive(Debug, PartialEq, Eq, Serialize)] + pub struct BarType { + #[serde(default, rename = "@a-list")] + pub a_list: ListType, + } + + #[derive(Debug, PartialEq, Eq, Serialize)] + pub struct ListType { + #[serde(default, rename = "$text")] + pub content: Vec, + } + + let foo = FooType { + a_list: ListType { + content: (vec!["A".to_string(), "B".to_string()]), + }, + }; + let bar = BarType { + a_list: ListType { + content: (vec!["A".to_string(), "B".to_string()]), + }, + }; + + let buffer = to_string_with_root("test", &foo).unwrap(); + assert_eq!(buffer, "A B".to_string()); + let buffer = to_string_with_root("test", &bar).unwrap(); + assert_eq!(buffer, "".to_string()); + } + } } From 8879d58a079c17a707c3fdd4bde4160874170eaa Mon Sep 17 00:00:00 2001 From: Jonas Dittrich Date: Thu, 30 Oct 2025 04:41:24 +0100 Subject: [PATCH 2/4] implement text-struct deserializer for simple types --- src/de/simple_type.rs | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/de/simple_type.rs b/src/de/simple_type.rs index 41db1273..c0e1efb4 100644 --- a/src/de/simple_type.rs +++ b/src/de/simple_type.rs @@ -3,13 +3,13 @@ //! [simple types]: https://www.w3schools.com/xml/el_simpletype.asp //! [as defined]: https://www.w3.org/TR/xmlschema11-1/#Simple_Type_Definition -use crate::de::Text; +use crate::de::{Text, TEXT_KEY}; use crate::encoding::Decoder; use crate::errors::serialize::DeError; use crate::escape::unescape; use crate::utils::CowRef; use memchr::memchr; -use serde::de::value::UnitDeserializer; +use serde::de::value::{MapDeserializer, SeqAccessDeserializer, UnitDeserializer}; use serde::de::{ DeserializeSeed, Deserializer, EnumAccess, IntoDeserializer, SeqAccess, VariantAccess, Visitor, }; @@ -727,7 +727,6 @@ impl<'de, 'a> Deserializer<'de> for SimpleTypeDeserializer<'de, 'a> { } unsupported!(deserialize_map); - unsupported!(deserialize_struct(&'static str, &'static [&'static str])); fn deserialize_enum( self, @@ -757,6 +756,33 @@ impl<'de, 'a> Deserializer<'de> for SimpleTypeDeserializer<'de, 'a> { { visitor.visit_unit() } + + fn deserialize_struct( + self, + name: &'static str, + fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + if fields == [TEXT_KEY] { + let content = match self.decode()? { + CowRef::Input(s) => Content::Input(s), + CowRef::Slice(s) => Content::Slice(s), + CowRef::Owned(s) => Content::Owned(s, 0), + }; + let list_iter = ListIter { + content: Some(content), + escaped: self.escaped, + }; + + let seq_deserializer = SeqAccessDeserializer::new(list_iter); + let der = MapDeserializer::new(std::iter::once((TEXT_KEY, seq_deserializer))); + return visitor.visit_map(der); + } + self.deserialize_str(visitor) + } } impl<'de, 'a> EnumAccess<'de> for SimpleTypeDeserializer<'de, 'a> { From 16159fe5e51836354dc527fb47486afe1b07606c Mon Sep 17 00:00:00 2001 From: Jonas Dittrich Date: Thu, 30 Oct 2025 04:41:58 +0100 Subject: [PATCH 3/4] test se and de --- tests/serde-issues.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ tests/serde-se.rs | 42 ------------------------------------------ 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/tests/serde-issues.rs b/tests/serde-issues.rs index 6e06f55b..6bf0cb65 100644 --- a/tests/serde-issues.rs +++ b/tests/serde-issues.rs @@ -709,3 +709,46 @@ fn issue888() { } ); } + +/// Regression test for https://github.com/tafia/quick-xml/issues/906. +#[test] +fn issue906() { + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct AsElement { + #[serde(rename = "a-list")] + a_list: TextContent, + } + + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct AsAttribute { + #[serde(rename = "@a-list")] + a_list: TextContent, + } + + #[derive(Debug, PartialEq, Deserialize, Serialize)] + pub struct TextContent { + #[serde(default, rename = "$text")] + content: Vec, + } + let foo = AsElement { + a_list: TextContent { + content: vec!["A".to_string(), "B".to_string()], + }, + }; + let bar = AsAttribute { + a_list: TextContent { + content: vec!["A".to_string(), "B".to_string()], + }, + }; + + let buffer = to_string_with_root("test", &foo).unwrap(); + std::assert_eq!(buffer, "A B"); + let foo2: AsElement = from_str(&buffer).unwrap(); + std::assert_eq!(foo2, foo); + + let buffer = to_string_with_root("test", &bar).unwrap(); + std::assert_eq!(buffer, ""); + + let bar2: AsAttribute = from_str(&buffer).unwrap(); + std::assert_eq!(bar2, bar); +} diff --git a/tests/serde-se.rs b/tests/serde-se.rs index 2f85b851..051e47c9 100644 --- a/tests/serde-se.rs +++ b/tests/serde-se.rs @@ -2295,46 +2295,4 @@ mod with_root { "); } } - - mod list { - use quick_xml::se::to_string_with_root; - use serde_derive::Serialize; - - #[test] - fn issue906() { - #[derive(Debug, PartialEq, Eq, Serialize)] - pub struct FooType { - #[serde(rename = "a-list")] - pub a_list: ListType, - } - - #[derive(Debug, PartialEq, Eq, Serialize)] - pub struct BarType { - #[serde(default, rename = "@a-list")] - pub a_list: ListType, - } - - #[derive(Debug, PartialEq, Eq, Serialize)] - pub struct ListType { - #[serde(default, rename = "$text")] - pub content: Vec, - } - - let foo = FooType { - a_list: ListType { - content: (vec!["A".to_string(), "B".to_string()]), - }, - }; - let bar = BarType { - a_list: ListType { - content: (vec!["A".to_string(), "B".to_string()]), - }, - }; - - let buffer = to_string_with_root("test", &foo).unwrap(); - assert_eq!(buffer, "A B".to_string()); - let buffer = to_string_with_root("test", &bar).unwrap(); - assert_eq!(buffer, "".to_string()); - } - } } From ea2277056c917b7328bccd965cd41e454802de24 Mon Sep 17 00:00:00 2001 From: Jonas Dittrich Date: Thu, 30 Oct 2025 12:49:20 +0100 Subject: [PATCH 4/4] flip call order --- src/de/simple_type.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/de/simple_type.rs b/src/de/simple_type.rs index c0e1efb4..bba14abe 100644 --- a/src/de/simple_type.rs +++ b/src/de/simple_type.rs @@ -779,7 +779,7 @@ impl<'de, 'a> Deserializer<'de> for SimpleTypeDeserializer<'de, 'a> { let seq_deserializer = SeqAccessDeserializer::new(list_iter); let der = MapDeserializer::new(std::iter::once((TEXT_KEY, seq_deserializer))); - return visitor.visit_map(der); + return der.deserialize_map(visitor); } self.deserialize_str(visitor) }