Skip to content

Commit 6176c33

Browse files
weiznichMingun
authored andcommitted
Properly use xsi:nil to deserialize null values via serde
This commit fixes an issue that causes `quick-xml` trying to deserialize empty tags via the serde interface even if these tags were explicitly marked as `xsi:nil="true"` For example the following XML failed to deserialize before this commit: ```xml <bar> <foo xsi:nil="true"/> </bar> ``` into the following rust type: ```rust #[derive(Deserialize)] struct Bar { foo: Option<Foo>, } #[derive(Deserialize)] struct Foo { baz: String, } ``` Before this commit this failed to deserialize with an error message that complained that the `baz` field was missing. After this commit this uses the `xsi:nil` attribute to deserialize this into `foo: None` instead. The standard (https://www.w3.org/TR/xmlschema-1/#xsi_nil) seems to support this behaviour. Fix #497
1 parent 10b37f7 commit 6176c33

File tree

3 files changed

+146
-5
lines changed

3 files changed

+146
-5
lines changed

Changelog.md

+2
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717

1818
- [#850]: Add `Attribute::as_bool()` method to get an attribute value as a boolean.
1919
- [#850]: Add `Attributes::has_nil()` method to check if attributes has `xsi:nil` attribute set to `true`.
20+
- [#497]: Handle `xsi:nil` attribute in serde Deserializer to better process optional fields.
2021

2122
### Bug Fixes
2223

2324
### Misc Changes
2425

26+
[#497]: https://github.com/tafia/quick-xml/issues/497
2527
[#850]: https://github.com/tafia/quick-xml/pull/850
2628

2729

src/de/map.rs

+22-1
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,21 @@ where
215215
has_value_field: fields.contains(&VALUE_KEY),
216216
})
217217
}
218+
219+
/// Determines if subtree started with the specified event shoould be skipped.
220+
///
221+
/// Used to map elements with `xsi:nil` attribute set to true to `None` in optional contexts.
222+
///
223+
/// We need to handle two attributes:
224+
/// - on parent element: <map xsi:nil="true"><foo/></map>
225+
/// - on this element: <map><foo xsi:nil="true"/></map>
226+
///
227+
/// We check parent element too because `xsi:nil` affects only nested elements of the
228+
/// tag where it is defined. We can map structure with fields mapped to attributes to
229+
/// the `<map>` element and set to `None` all its optional elements.
230+
fn should_skip_subtree(&self, start: &BytesStart) -> bool {
231+
self.de.reader.reader.has_nil_attr(&self.start) || self.de.reader.reader.has_nil_attr(start)
232+
}
218233
}
219234

220235
impl<'de, 'd, R, E> MapAccess<'de> for ElementMapAccess<'de, 'd, R, E>
@@ -540,8 +555,14 @@ where
540555
where
541556
V: Visitor<'de>,
542557
{
543-
match self.map.de.peek()? {
558+
// We cannot use result of `peek()` directly because of borrow checker
559+
let _ = self.map.de.peek()?;
560+
match self.map.de.last_peeked() {
544561
DeEvent::Text(t) if t.is_empty() => visitor.visit_none(),
562+
DeEvent::Start(start) if self.map.should_skip_subtree(start) => {
563+
self.map.de.skip_next_tree()?;
564+
visitor.visit_none()
565+
}
545566
_ => visitor.visit_some(self),
546567
}
547568
}

src/de/mod.rs

+122-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
//! - [Optional attributes and elements](#optional-attributes-and-elements)
2020
//! - [Choices (`xs:choice` XML Schema type)](#choices-xschoice-xml-schema-type)
2121
//! - [Sequences (`xs:all` and `xs:sequence` XML Schema types)](#sequences-xsall-and-xssequence-xml-schema-types)
22+
//! - [Mapping of `xsi:nil`](#mapping-of-xsinil)
2223
//! - [Generate Rust types from XML](#generate-rust-types-from-xml)
2324
//! - [Composition Rules](#composition-rules)
2425
//! - [Enum Representations](#enum-representations)
@@ -413,6 +414,13 @@
413414
//! <any-tag optional=""/> <!-- Some("") -->
414415
//! <any-tag/> <!-- None -->
415416
//! ```
417+
//! <div style="background:rgba(120,145,255,0.45);padding:0.75em;">
418+
//!
419+
//! NOTE: The behaviour is not symmetric by default. `None` will be serialized as
420+
//! `optional=""`. This behaviour is consistent across serde crates. You should add
421+
//! `#[serde(skip_serializing_if = "Option::is_none")]` attribute to the field to
422+
//! skip `None`s.
423+
//! </div>
416424
//! </td>
417425
//! </tr>
418426
//! <!-- 7 ===================================================================================== -->
@@ -454,9 +462,15 @@
454462
//! When the XML element is present, type `T` will be deserialized from an
455463
//! element (which is a string or a multi-mapping -- i.e. mapping which can have
456464
//! duplicated keys).
457-
//! <div style="background:rgba(80, 240, 100, 0.20);padding:0.75em;">
465+
//! <div style="background:rgba(120,145,255,0.45);padding:0.75em;">
458466
//!
459-
//! Currently some edge cases exists described in the issue [#497].
467+
//! NOTE: The behaviour is not symmetric by default. `None` will be serialized as
468+
//! `<optional/>`. This behaviour is consistent across serde crates. You should add
469+
//! `#[serde(skip_serializing_if = "Option::is_none")]` attribute to the field to
470+
//! skip `None`s.
471+
//!
472+
//! NOTE: Deserializer will automatically handle a [`xsi:nil`] attribute and set field to `None`.
473+
//! For more info see [Mapping of `xsi:nil`](#mapping-of-xsinil).
460474
//! </div>
461475
//! </td>
462476
//! </tr>
@@ -1312,6 +1326,65 @@
13121326
//! </table>
13131327
//!
13141328
//!
1329+
//! Mapping of `xsi:nil`
1330+
//! ====================
1331+
//!
1332+
//! quick-xml supports handling of [`xsi:nil`] special attribute. When field of optional
1333+
//! type is mapped to the XML element which have `xsi:nil="true"` set, or if that attribute
1334+
//! is placed on parent XML element, the deserializer will call [`Visitor::visit_none`]
1335+
//! and skip XML element corresponding to a field.
1336+
//!
1337+
//! Examples:
1338+
//!
1339+
//! ```
1340+
//! # use pretty_assertions::assert_eq;
1341+
//! # use serde::Deserialize;
1342+
//! #[derive(Deserialize, Debug, PartialEq)]
1343+
//! struct TypeWithOptionalField {
1344+
//! element: Option<String>,
1345+
//! }
1346+
//!
1347+
//! assert_eq!(
1348+
//! TypeWithOptionalField {
1349+
//! element: None,
1350+
//! },
1351+
//! quick_xml::de::from_str("
1352+
//! <any-tag xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
1353+
//! <element xsi:nil='true'>Content is skiped because of xsi:nil='true'</element>
1354+
//! </any-tag>
1355+
//! ").unwrap(),
1356+
//! );
1357+
//! ```
1358+
//!
1359+
//! You can capture attributes from the optional type, because ` xsi:nil="true"` elements can have
1360+
//! attributes:
1361+
//! ```
1362+
//! # use pretty_assertions::assert_eq;
1363+
//! # use serde::Deserialize;
1364+
//! #[derive(Deserialize, Debug, PartialEq)]
1365+
//! struct TypeWithOptionalField {
1366+
//! #[serde(rename = "@attribute")]
1367+
//! attribute: usize,
1368+
//!
1369+
//! element: Option<String>,
1370+
//! non_optional: String,
1371+
//! }
1372+
//!
1373+
//! assert_eq!(
1374+
//! TypeWithOptionalField {
1375+
//! attribute: 42,
1376+
//! element: None,
1377+
//! non_optional: "Note, that non-optional fields will be deserialized as usual".to_string(),
1378+
//! },
1379+
//! quick_xml::de::from_str("
1380+
//! <any-tag attribute='42' xsi:nil='true' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
1381+
//! <element>Content is skiped because of xsi:nil='true'</element>
1382+
//! <non_optional>Note, that non-optional fields will be deserialized as usual</non_optional>
1383+
//! </any-tag>
1384+
//! ").unwrap(),
1385+
//! );
1386+
//! ```
1387+
//!
13151388
//! Generate Rust types from XML
13161389
//! ============================
13171390
//!
@@ -1820,7 +1893,7 @@
18201893
//! [`overlapped-lists`]: ../index.html#overlapped-lists
18211894
//! [specification]: https://www.w3.org/TR/xmlschema11-1/#Simple_Type_Definition
18221895
//! [`deserialize_with`]: https://serde.rs/field-attrs.html#deserialize_with
1823-
//! [#497]: https://github.com/tafia/quick-xml/issues/497
1896+
//! [`xsi:nil`]: https://www.w3.org/TR/xmlschema-1/#xsi_nil
18241897
//! [`Serializer::serialize_unit_variant`]: serde::Serializer::serialize_unit_variant
18251898
//! [`Deserializer::deserialize_enum`]: serde::Deserializer::deserialize_enum
18261899
//! [`SeError::Unsupported`]: crate::errors::serialize::SeError::Unsupported
@@ -2534,6 +2607,22 @@ where
25342607
}
25352608
}
25362609

2610+
#[inline]
2611+
fn last_peeked(&self) -> &DeEvent<'de> {
2612+
#[cfg(feature = "overlapped-lists")]
2613+
{
2614+
self.read
2615+
.front()
2616+
.expect("`Deserializer::peek()` should be called")
2617+
}
2618+
#[cfg(not(feature = "overlapped-lists"))]
2619+
{
2620+
self.peek
2621+
.as_ref()
2622+
.expect("`Deserializer::peek()` should be called")
2623+
}
2624+
}
2625+
25372626
fn next(&mut self) -> Result<DeEvent<'de>, DeError> {
25382627
// Replay skipped or peeked events
25392628
#[cfg(feature = "overlapped-lists")]
@@ -2764,6 +2853,14 @@ where
27642853
}
27652854
self.reader.read_to_end(name)
27662855
}
2856+
2857+
fn skip_next_tree(&mut self) -> Result<(), DeError> {
2858+
let DeEvent::Start(start) = self.next()? else {
2859+
unreachable!("Only call this if the next event is a start event")
2860+
};
2861+
let name = start.name();
2862+
self.read_to_end(name)
2863+
}
27672864
}
27682865

27692866
impl<'de> Deserializer<'de, SliceReader<'de>> {
@@ -2945,9 +3042,16 @@ where
29453042
where
29463043
V: Visitor<'de>,
29473044
{
2948-
match self.peek()? {
3045+
// We cannot use result of `peek()` directly because of borrow checker
3046+
let _ = self.peek()?;
3047+
match self.last_peeked() {
29493048
DeEvent::Text(t) if t.is_empty() => visitor.visit_none(),
29503049
DeEvent::Eof => visitor.visit_none(),
3050+
// if the `xsi:nil` attribute is set to true we got a none value
3051+
DeEvent::Start(start) if self.reader.reader.has_nil_attr(&start) => {
3052+
self.skip_next_tree()?;
3053+
visitor.visit_none()
3054+
}
29513055
_ => visitor.visit_some(self),
29523056
}
29533057
}
@@ -3071,6 +3175,12 @@ pub trait XmlRead<'i> {
30713175

30723176
/// A copy of the reader's decoder used to decode strings.
30733177
fn decoder(&self) -> Decoder;
3178+
3179+
/// Checks if the `start` tag has a [`xsi:nil`] attribute. This method ignores
3180+
/// any errors in attributes.
3181+
///
3182+
/// [`xsi:nil`]: https://www.w3.org/TR/xmlschema-1/#xsi_nil
3183+
fn has_nil_attr(&self, start: &BytesStart) -> bool;
30743184
}
30753185

30763186
/// XML input source that reads from a std::io input stream.
@@ -3140,6 +3250,10 @@ impl<'i, R: BufRead> XmlRead<'i> for IoReader<R> {
31403250
fn decoder(&self) -> Decoder {
31413251
self.reader.decoder()
31423252
}
3253+
3254+
fn has_nil_attr(&self, start: &BytesStart) -> bool {
3255+
start.attributes().has_nil(&self.reader)
3256+
}
31433257
}
31443258

31453259
/// XML input source that reads from a slice of bytes and can borrow from it.
@@ -3205,6 +3319,10 @@ impl<'de> XmlRead<'de> for SliceReader<'de> {
32053319
fn decoder(&self) -> Decoder {
32063320
self.reader.decoder()
32073321
}
3322+
3323+
fn has_nil_attr(&self, start: &BytesStart) -> bool {
3324+
start.attributes().has_nil(&self.reader)
3325+
}
32083326
}
32093327

32103328
#[cfg(test)]

0 commit comments

Comments
 (0)