Skip to content

Conversation

@Kakadus
Copy link

@Kakadus Kakadus commented Oct 22, 2025

close #906

allows structs that contain only one $text field to be serialized as an attribute list.

As this is my first contribution, I may have overlooked something. The tests seem to pass though :)

@Kakadus Kakadus changed the title Fix serialization of structs with only text content Fix serialization of structs with only text content as attributes Oct 22, 2025
@Mingun Mingun added enhancement serde Issues related to mapping from Rust types to XML arrays Issues related to mapping XML content onto arrays using serde labels Oct 23, 2025
@codecov-commenter
Copy link

codecov-commenter commented Oct 23, 2025

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 90.74074% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.28%. Comparing base (38b44d4) to head (ea22770).
⚠️ Report is 46 commits behind head on master.

Files with missing lines Patch % Lines
src/se/simple_type.rs 90.32% 3 Missing ⚠️
src/de/simple_type.rs 91.30% 2 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #907      +/-   ##
==========================================
+ Coverage   55.52%   58.28%   +2.75%     
==========================================
  Files          42       42              
  Lines       15511    15599      +88     
==========================================
+ Hits         8613     9092     +479     
+ Misses       6898     6507     -391     
Flag Coverage Δ
unittests 58.28% <90.74%> (+2.75%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Mingun
Copy link
Collaborator

Mingun commented Oct 23, 2025

I think, that makes sense.

@Mingun
Copy link
Collaborator

Mingun commented Oct 25, 2025

When polishing this PR I found, that such implementation creates inconsistency: you cannot parse struct back from the attribute value. That may be solved if consider _fields here:

quick-xml/src/de/text.rs

Lines 124 to 138 in 655691c

#[inline]
fn deserialize_struct<V>(
self,
_name: &'static str,
_fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
// Deserializer methods are only hints, if deserializer could not satisfy
// request, it should return the data that it has. It is responsibility
// of a Visitor to return an error if it does not understand the data
self.deserialize_str(visitor)
}

but I prefer not to do that. As you known, some serde attributes may change struct representation to map representation and therefore we will lose our hack here, because maps does not report their keys.

It seems, that in that case it is better to use #[serde(with)] and convert you struct into/from string manually. What do you think?

Below the adapted test which failed even with this PR:

/// 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)]
    struct TextContent {
        #[serde(rename = "$text")]
        content: Vec<String>,
        #[serde(skip)]
        ignored: (),
    }

    let foo = AsElement {
        a_list: TextContent {
            content: vec!["A".to_string(), "B".to_string()],
            ignored: (),
        },
    };
    let bar = AsAttribute {
        a_list: TextContent {
            content: vec!["A".to_string(), "B".to_string()],
            ignored: (),
        },
    };

    let buffer = to_string_with_root("test", &foo).unwrap();
    assert_eq!(buffer, "<test><a-list>A B</a-list></test>");
    let foo2: AsElement = from_str(&buffer).unwrap();
    assert_eq!(foo2, foo);

    let buffer = to_string_with_root("test", &bar).unwrap();
    assert_eq!(buffer, "<test a-list=\"A B\"/>");

    // thread 'issue906' panicked at tests\serde-issues.rs:756:47:
    // called `Result::unwrap()` on an `Err` value: Custom("invalid type: string \"A B\", expected struct TextContent")
    let bar2: AsAttribute = from_str(&buffer).unwrap();
    assert_eq!(bar2, bar);
}

For element there is no problem, because TextContent are deserialized from <a-list>A B</a-list> and not just from "A B".

@Kakadus
Copy link
Author

Kakadus commented Oct 30, 2025

Thanks for taking a look! It seems that I simply forgot about deserialization :)

Writing custom (de)serializers always work, although I'd really like to see first party support for all usages of the text structs. Each struct would need their own implementation, and you'd have to track their usage and apply the with appropriately. Coming from code generators like https://docs.rs/xsd-parser/latest/xsd_parser/, this makes it very complicated IMHO. What do you think about the SimpleTypeDeserializer extension I commited instead?

As you known, some serde attributes may change struct representation to map representation and therefore we will lose our hack here, because maps does not report their keys.

Do you mean e.g. #[serde(flatten)]? A short experiment shows that deserialization of AsElement fails already (which is not affected by this PR):

        /// 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)]
            pub struct TextContent {
                #[serde(flatten)]
                content: Inner,
            }

            #[derive(Debug, PartialEq, Deserialize, Serialize)]
            pub struct Inner {
                #[serde(default, rename = "$text")]
                content: Vec<String>,
            }

            let foo = AsElement {
                a_list: TextContent {
                    content: Inner {
                        content: vec!["A".to_string(), "B".to_string()],
                    },
                },
            };

            let buffer = to_string_with_root("test", &foo).unwrap();
            assert_eq!(buffer, "<test><a-list>A B</a-list></test>");
            // thread 'with_root::list::issue906' panicked at tests/serde-se.rs:2347:53:
            // called `Result::unwrap()` on an `Err` value: Custom("invalid type: string \"A B\", expected a sequence")
            let foo2: AsElement = from_str(&buffer).unwrap();
            assert_eq!(foo2, foo);
        }

@Kakadus
Copy link
Author

Kakadus commented Oct 30, 2025

build error with old serde version should be fixed now

I'm afraid the current approach requires a bump of serde to have your IntoDeserializer for SeqAccessDeserializer from serde 1.0.214...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arrays Issues related to mapping XML content onto arrays using serde enhancement serde Issues related to mapping from Rust types to XML

Projects

None yet

Development

Successfully merging this pull request may close these issues.

struct-based list can be serde-serialized as child but not as attribute

3 participants