Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ struct and can be applied at once. When `serde-types` feature is enabled, config
XHTML.
- [#846]: Add method `empty_element_handling()` as a more powerful alternative to `expand_empty_elements()`
in `Serializer`.
- [#929]: Allow to pass list of field names to `impl_deserialize_for_internally_tagged_enum!` macro
which is required if you enum variants contains `$value` fields.

### Bug Fixes

Expand All @@ -42,8 +44,9 @@ struct and can be applied at once. When `serde-types` feature is enabled, config
[#846]: https://github.com/tafia/quick-xml/issues/846
[#908]: https://github.com/tafia/quick-xml/pull/908
[#913]: https://github.com/tafia/quick-xml/pull/913
[#924]: https://github.com/tafia/quick-xml/pull/924
[#923]: https://github.com/tafia/quick-xml/issues/923
[#924]: https://github.com/tafia/quick-xml/pull/924
[#929]: https://github.com/tafia/quick-xml/pull/929


## 0.38.4 -- 2025-11-11
Expand Down
84 changes: 82 additions & 2 deletions src/serde_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ macro_rules! deserialize_match {
/// }
///
/// #[derive(Debug, PartialEq)]
/// // #[serde(tag = "@tag")]
/// enum InternallyTaggedEnum {
/// NewType(Newtype),
/// Other,
Expand Down Expand Up @@ -222,13 +223,92 @@ macro_rules! deserialize_match {
/// );
/// ```
///
/// If some struct or newtype variants have the specially named `$text` or `$value` fields,
/// you need to say that to the generated `Deserialize` implementation. XML deserializer
/// uses presence of that fields to determine if it need to emit such keys. You may specify,
/// which keys should be available in square brackets just after the tag name and colon:
///
/// ```
/// # use pretty_assertions::assert_eq;
/// use quick_xml::de::from_str;
/// use quick_xml::impl_deserialize_for_internally_tagged_enum;
/// use serde::Deserialize;
///
/// #[derive(Deserialize, Debug, PartialEq)]
/// struct Root {
/// one: InternallyTaggedEnum,
/// two: InternallyTaggedEnum,
/// }
///
/// #[derive(Deserialize, Debug, PartialEq)]
/// enum ExternallyTaggedEnum {
/// First,
/// Second,
/// }
/// #[derive(Debug, PartialEq)]
/// // #[serde(tag = "@tag")]
/// enum InternallyTaggedEnum {
/// NewType(Newtype),
/// Struct {
/// // #[serde(rename = "$value")]
/// any: ExternallyTaggedEnum,
/// },
/// }
///
/// #[derive(Deserialize, Debug, PartialEq)]
/// struct Newtype {
/// #[serde(rename = "$value")]
/// any: ExternallyTaggedEnum,
/// }
///
/// // The macro needs the type of the enum, the tag name, the list of fields,
/// // and information about all the variants
/// impl_deserialize_for_internally_tagged_enum!{
/// // Without "$value" you get
/// // called `Result::unwrap()` on an `Err` value: Custom("missing field `$value`")
/// // That list will be passed to `deserialize_struct`
/// InternallyTaggedEnum, "@tag": ["$value"],
///
/// ("NewType" => NewType(Newtype)),
/// ("Struct" => Struct {
/// #[serde(rename = "$value")]
/// any: ExternallyTaggedEnum,
/// }),
/// }
///
/// assert_eq!(
/// from_str::<Root>(r#"
/// <root>
/// <one tag="NewType">
/// <First />
/// </one>
/// <two tag="Struct">
/// <Second />
/// </two>
/// </root>
/// "#).unwrap(),
/// Root {
/// one: InternallyTaggedEnum::NewType(Newtype { any: ExternallyTaggedEnum::First }),
/// two: InternallyTaggedEnum::Struct { any: ExternallyTaggedEnum::Second },
/// },
/// );
/// ```
/// <div style="background:rgba(120,145,255,0.45);padding:0.75em;">
///
/// NOTE: In addition to `$value` you must specify _all_ field names that may appear
/// in every variant! Otherwise you'll get ```Custom("missing field `unlisted field name`")```
/// error. That is because XML tags for all unlisted field names would be mapped to field `$value`.
/// If your types do not have a `$value` special field, you may omit the field list.
///
/// </div>
///
/// [internally tagged]: https://serde.rs/enum-representations.html#internally-tagged
/// [serde#1183]: https://github.com/serde-rs/serde/issues/1183
#[macro_export(local_inner_macros)]
macro_rules! impl_deserialize_for_internally_tagged_enum {
(
$enum:ty,
$tag:literal,
$tag:literal $(:[ $($field:literal),* ])?,
$($cases:tt)*
) => {
impl<'de> serde::de::Deserialize<'de> for $enum {
Expand Down Expand Up @@ -285,7 +365,7 @@ macro_rules! impl_deserialize_for_internally_tagged_enum {
}
// Tell the deserializer to deserialize the data as a map,
// using the TheVisitor as the decoder
deserializer.deserialize_map(TheVisitor)
deserializer.deserialize_struct(std::stringify!($enum), &[ $($($field),*)? ], TheVisitor)
}
}
}
Expand Down
139 changes: 139 additions & 0 deletions tests/serde-issues.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,3 +709,142 @@ fn issue888() {
}
);
}

/// Regression test for https://github.com/tafia/quick-xml/issues/928.
#[cfg(feature = "serde-types")]
#[test]
fn issue928() {
use quick_xml::impl_deserialize_for_internally_tagged_enum;

#[derive(Debug, Deserialize, PartialEq)]
struct Root {
#[serde(rename = "action")]
actions: Vec<Action>,
}

#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
enum Element {
Node {
#[serde(rename = "@id")]
id: u64,
#[serde(rename = "tag")]
tags: Option<Vec<Tag>>,
},
Way,
#[serde(other)]
Other,
}

#[derive(Debug, Deserialize, Clone, PartialEq)]
struct Tag {
#[serde(rename = "@k")]
k: String,
#[serde(rename = "@v")]
v: String,
}

#[derive(Debug, Deserialize, PartialEq)]
struct ElementHolder {
#[serde(rename = "$value")]
e: Element,
}

#[derive(Debug, PartialEq)]
enum Action {
Create(ElementHolder),
Modify {
old: ElementHolder,
new: ElementHolder,
},
Delete {
old: ElementHolder,
new: ElementHolder,
},
}

impl_deserialize_for_internally_tagged_enum! {
Action, "@type": ["$value", "old", "new"],
("create" => Create(ElementHolder)),
("modify" => Modify {
old: ElementHolder,
new: ElementHolder
}),
("delete" => Delete {
old: ElementHolder,
new: ElementHolder
})
}

assert_eq!(
from_str::<Root>(
r#"
<root>
<action type="create">
<node id="123">
<tag k="key" v="value"/>
</node>
</action>
<action type="modify">
<old>
<node id="456">
<tag k="key" v="old_value"/>
</node>
</old>
<new>
<node id="456">
<tag k="key" v="new_value"/>
</node>
</new>
</action>
<action type="modify">
<old>
<way />
</old>
<new>
<way />
</new>
</action>
</root>
"#
)
.unwrap(),
Root {
actions: vec![
Action::Create(ElementHolder {
e: Element::Node {
id: 123,
tags: Some(vec![Tag {
k: "key".to_string(),
v: "value".to_string(),
}]),
},
}),
Action::Modify {
old: ElementHolder {
e: Element::Node {
id: 456,
tags: Some(vec![Tag {
k: "key".to_string(),
v: "old_value".to_string(),
}]),
},
},
new: ElementHolder {
e: Element::Node {
id: 456,
tags: Some(vec![Tag {
k: "key".to_string(),
v: "new_value".to_string(),
}]),
},
},
},
Action::Modify {
old: ElementHolder { e: Element::Way },
new: ElementHolder { e: Element::Way },
},
],
},
);
}