quick-xml
quick-xml copied to clipboard
Error when Deserializing Nested Enum Fields
I'm trying to deserialize an XML document with the following structure, where inside the Id
tag, there can be a choice of two elements.
<Entity>
<Id>
<A>Id</A>
</Id>
</Entity>
I'm trying to deserialize this with the following setup:
use serde::Deserialize;
use std::error::Error;
#[derive(Debug, Deserialize)]
struct Entity {
id: IdType,
}
#[derive(Debug, Deserialize)]
enum IdType {
A(String),
B(String),
}
fn main() -> Result<(), Box<dyn Error>> {
let e: Entity = quick_xml::de::from_str("<entity><id><a>Id</a></id></entity>")?;
dbg!(e);
Ok(())
}
However, when I run this, I get an error Error: Custom("unknown variant `id`, expected `A` or `B`")
.
I would expect the document to deserialize to Entity { id: IdType::A("Id") }
.
I've tried this with quick-xml
versions 0.22.0
, 0.23.0-alpha
, 0.23.0-alpha3
, and the latest master, the behavior is the same with all.
I've also tried renaming the field using $unflatten=Id
, but it did not change the error.
For comparison, the same document deserializes with serde-xml-rs
.
I've now also tried the fork from #183, but the error stays.
The correct structure would be:
#[test]
fn issue349() {
#[derive(Debug, Deserialize, Serialize, PartialEq)]
struct Entity {
id: Id,
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
struct Id {
// #[serde(rename = "#content")] // After #490 is merged
#[serde(rename = "$value")] // Before #490 is merged
content: Enum,
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
enum Enum {
A(String),
B(String),
}
assert_eq!(
from_str::<Entity>("<entity><id><a>Id</a></id></entity>").unwrap(),
Entity {
id: Id {
content: Enum::A("Id".to_string()),
}
}
);
}
The XML mapped to the types in the following manner: Legend:
-
^^^
-- bytes, used to deserialize content -
~~~
-- helper bytes, that supports XML structure, does not matter for deserialized type
Your structs mapped as:
<entity><id><a>Id</a></id></entity>
^^^^^^^^^^^^^^^^^^ IdType, IdType::id (non-existent, so the error)
^^^^ ^^^^^ Entity::id
~~~~~~~~^^^^^^^^^^^^^^^^^^~~~~~~~~~ Entity
How to build correct mapping? Start constructing with only structs. Remember, that struct
is mapped to <>...</>
, and each struct field is mapped to the one child XML element inside your XML fragment:
// <entity>...</entity>
struct Entity {
id: Id,
}
// <id>...</id>
struct Id {
a: String,
}
// rust std
// <a>...</a>
struct String {...}
Start construction from outermost XML fragment, extracting pieces and create fields in your struct. Follow that guide image:
<entity><id><a>Id</a></id></entity>
~~~^^~~~~ String
^^^ ^^^^ Id::a
~~~~^^^^^^^^^~~~~~ Id
^^^^ ^^^^^ Entity::id
~~~~~~~~^^^^^^^^^^^^^^^^^^~~~~~~~~~ Entity
String
is a fundamental type for which quick-xml implements mapping from any XML fragment, like <>text or CDATA content</>
. Names of elements of XML fragment is ignored.
Then determine, where in the XML element names, corresponding to your enum variants is located. In that case it is <a>...</a>
. So <a>...</a>
is mapped to IdType::A
, and IdType
mapped to the same span.
Then, you see, that your IdType
type would be wrapped in <id>...</id>
with a key a
. Because key has significant meaning for a IdType
(it is discriminant of variant) we need a dynamically named field. That is achieved by adding #[serde(rename = "#content")]
(#[serde(rename = "$value")]
before #490 is merged) to a field a
. At that stage our types are:
// <id>...</id>
struct Id {
#[serde(rename = "#content")]
a: IdType,
}
// <a>...</a>
// <b>...</b>
// etc.
enum IdType {
a(String),
b(String),
}
// rust std
// <a>...</a>
struct String {...}
The last step wrap the Id
struct to the one more struct with id
field and we could rename field Id::a
to Id::content
(because rust name in any case is not used for serialization purposes) and add rename rule to the enum variants to match rust style for names. In the end we get a final result from the beginning of this message.