Repository: tafia/quick-xml
Pull request #850 (2025-03-21 16:28:38)

Properly use xsi:nil to deserialize null values via serde
Supersedes #842 because I cannot push to it directly.

@weiznich, thanks for your contribution! I finally found the time to finish and polish your work. If you have anything to say about the proposed mapping scheme, feel free to.

Here is the benchmark results of `compare` subproject between master (c8fe85005063076a6df28ef3223718d5696e391f) and namespace-aware deserializer (b8ce862a4d970723541c20bfc7c14fa898cde475):
[criterion-namespace-aware-deserializer.zip](https://github.com/user-attachments/files/19394163/criterion-namespace-aware-deserializer.zip)


Patch:
diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml
index dc4feb7..33eb429 100644
--- a/.github/workflows/cifuzz.yml
+++ b/.github/workflows/cifuzz.yml
@@ -17,7 +17,7 @@ jobs:
        language: rust
        fuzz-seconds: 600
    - name: Upload Crash
-     uses: actions/upload-artifact@v3
+     uses: actions/upload-artifact@v4
      if: failure() && steps.build.outcome == 'success'
      with:
        name: artifacts
diff --git a/Cargo.toml b/Cargo.toml
index 793324a..6ba8116 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -231,6 +231,11 @@ name = "serde-de-seq"
 required-features = ["serialize"]
 path = "tests/serde-de-seq.rs"
 
+[[test]]
+name = "serde-de-xsi"
+required-features = ["serialize"]
+path = "tests/serde-de-xsi.rs"
+
 [[test]]
 name = "serde-se"
 required-features = ["serialize"]
diff --git a/Changelog.md b/Changelog.md
index 7636b5f..1d0605a 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -15,10 +15,17 @@
 
 ### New Features
 
+- [#850]: Add `Attribute::as_bool()` method to get an attribute value as a boolean.
+- [#850]: Add `Attributes::has_nil()` method to check if attributes has `xsi:nil` attribute set to `true`.
+- [#497]: Handle `xsi:nil` attribute in serde Deserializer to better process optional fields.
+
 ### Bug Fixes
 
 ### Misc Changes
 
+[#497]: https://github.com/tafia/quick-xml/issues/497
+[#850]: https://github.com/tafia/quick-xml/pull/850
+
 
 ## 0.37.2 -- 2024-12-29
 
diff --git a/src/de/map.rs b/src/de/map.rs
index 9f2a987..7165f6a 100644
--- a/src/de/map.rs
+++ b/src/de/map.rs
@@ -215,6 +215,21 @@ where
             has_value_field: fields.contains(&VALUE_KEY),
         })
     }
+
+    /// Determines if subtree started with the specified event shoould be skipped.
+    ///
+    /// Used to map elements with `xsi:nil` attribute set to true to `None` in optional contexts.
+    ///
+    /// We need to handle two attributes:
+    /// - on parent element: <map xsi:nil="true"><foo/></map>
+    /// - on this element:   <map><foo xsi:nil="true"/></map>
+    ///
+    /// We check parent element too because `xsi:nil` affects only nested elements of the
+    /// tag where it is defined. We can map structure with fields mapped to attributes to
+    /// the `<map>` element and set to `None` all its optional elements.
+    fn should_skip_subtree(&self, start: &BytesStart) -> bool {
+        self.de.reader.reader.has_nil_attr(&self.start) || self.de.reader.reader.has_nil_attr(start)
+    }
 }
 
 impl<'de, 'd, R, E> MapAccess<'de> for ElementMapAccess<'de, 'd, R, E>
@@ -540,8 +555,14 @@ where
     where
         V: Visitor<'de>,
     {
-        match self.map.de.peek()? {
+        // We cannot use result of `peek()` directly because of borrow checker
+        let _ = self.map.de.peek()?;
+        match self.map.de.last_peeked() {
             DeEvent::Text(t) if t.is_empty() => visitor.visit_none(),
+            DeEvent::Start(start) if self.map.should_skip_subtree(start) => {
+                self.map.de.skip_next_tree()?;
+                visitor.visit_none()
+            }
             _ => visitor.visit_some(self),
         }
     }
diff --git a/src/de/mod.rs b/src/de/mod.rs
index 484c31b..5f18e91 100644
--- a/src/de/mod.rs
+++ b/src/de/mod.rs
@@ -19,6 +19,7 @@
 //!   - [Optional attributes and elements](#optional-attributes-and-elements)
 //!   - [Choices (`xs:choice` XML Schema type)](#choices-xschoice-xml-schema-type)
 //!   - [Sequences (`xs:all` and `xs:sequence` XML Schema types)](#sequences-xsall-and-xssequence-xml-schema-types)
+//! - [Mapping of `xsi:nil`](#mapping-of-xsinil)
 //! - [Generate Rust types from XML](#generate-rust-types-from-xml)
 //! - [Composition Rules](#composition-rules)
 //! - [Enum Representations](#enum-representations)
@@ -413,6 +414,13 @@
 //! <any-tag optional=""/>   <!-- Some("") -->
 //! <any-tag/>               <!-- None -->
 //! ```
+//! <div style="background:rgba(120,145,255,0.45);padding:0.75em;">
+//!
+//! NOTE: The behaviour is not symmetric by default. `None` will be serialized as
+//! `optional=""`. This behaviour is consistent across serde crates. You should add
+//! `#[serde(skip_serializing_if = "Option::is_none")]` attribute to the field to
+//! skip `None`s.
+//! </div>
 //! </td>
 //! </tr>
 //! <!-- 7 ===================================================================================== -->
@@ -454,9 +462,15 @@
 //! When the XML element is present, type `T` will be deserialized from an
 //! element (which is a string or a multi-mapping -- i.e. mapping which can have
 //! duplicated keys).
-//! <div style="background:rgba(80, 240, 100, 0.20);padding:0.75em;">
+//! <div style="background:rgba(120,145,255,0.45);padding:0.75em;">
 //!
-//! Currently some edge cases exists described in the issue [#497].
+//! NOTE: The behaviour is not symmetric by default. `None` will be serialized as
+//! `<optional/>`. This behaviour is consistent across serde crates. You should add
+//! `#[serde(skip_serializing_if = "Option::is_none")]` attribute to the field to
+//! skip `None`s.
+//!
+//! NOTE: Deserializer will automatically handle a [`xsi:nil`] attribute and set field to `None`.
+//! For more info see [Mapping of `xsi:nil`](#mapping-of-xsinil).
 //! </div>
 //! </td>
 //! </tr>
@@ -1312,6 +1326,65 @@
 //! </table>
 //!
 //!
+//! Mapping of `xsi:nil`
+//! ====================
+//!
+//! quick-xml supports handling of [`xsi:nil`] special attribute. When field of optional
+//! type is mapped to the XML element which have `xsi:nil="true"` set, or if that attribute
+//! is placed on parent XML element, the deserializer will call [`Visitor::visit_none`]
+//! and skip XML element corresponding to a field.
+//!
+//! Examples:
+//!
+//! ```
+//! # use pretty_assertions::assert_eq;
+//! # use serde::Deserialize;
+//! #[derive(Deserialize, Debug, PartialEq)]
+//! struct TypeWithOptionalField {
+//!   element: Option<String>,
+//! }
+//!
+//! assert_eq!(
+//!   TypeWithOptionalField {
+//!     element: None,
+//!   },
+//!   quick_xml::de::from_str("
+//!     <any-tag xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
+//!       <element xsi:nil='true'>Content is skiped because of xsi:nil='true'</element>
+//!     </any-tag>
+//!   ").unwrap(),
+//! );
+//! ```
+//!
+//! You can capture attributes from the optional type, because ` xsi:nil="true"` elements can have
+//! attributes:
+//! ```
+//! # use pretty_assertions::assert_eq;
+//! # use serde::Deserialize;
+//! #[derive(Deserialize, Debug, PartialEq)]
+//! struct TypeWithOptionalField {
+//!   #[serde(rename = "@attribute")]
+//!   attribute: usize,
+//!
+//!   element: Option<String>,
+//!   non_optional: String,
+//! }
+//!
+//! assert_eq!(
+//!   TypeWithOptionalField {
+//!     attribute: 42,
+//!     element: None,
+//!     non_optional: "Note, that non-optional fields will be deserialized as usual".to_string(),
+//!   },
+//!   quick_xml::de::from_str("
+//!     <any-tag attribute='42' xsi:nil='true' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
+//!       <element>Content is skiped because of xsi:nil='true'</element>
+//!       <non_optional>Note, that non-optional fields will be deserialized as usual</non_optional>
+//!     </any-tag>
+//!   ").unwrap(),
+//! );
+//! ```
+//!
 //! Generate Rust types from XML
 //! ============================
 //!
@@ -1820,7 +1893,7 @@
 //! [`overlapped-lists`]: ../index.html#overlapped-lists
 //! [specification]: https://www.w3.org/TR/xmlschema11-1/#Simple_Type_Definition
 //! [`deserialize_with`]: https://serde.rs/field-attrs.html#deserialize_with
-//! [#497]: https://github.com/tafia/quick-xml/issues/497
+//! [`xsi:nil`]: https://www.w3.org/TR/xmlschema-1/#xsi_nil
 //! [`Serializer::serialize_unit_variant`]: serde::Serializer::serialize_unit_variant
 //! [`Deserializer::deserialize_enum`]: serde::Deserializer::deserialize_enum
 //! [`SeError::Unsupported`]: crate::errors::serialize::SeError::Unsupported
@@ -2016,7 +2089,7 @@ use crate::{
     errors::Error,
     events::{BytesCData, BytesEnd, BytesStart, BytesText, Event},
     name::QName,
-    reader::Reader,
+    reader::NsReader,
     utils::CowRef,
 };
 use serde::de::{
@@ -2415,7 +2488,7 @@ where
     /// # use pretty_assertions::assert_eq;
     /// use serde::Deserialize;
     /// use quick_xml::de::Deserializer;
-    /// use quick_xml::Reader;
+    /// use quick_xml::NsReader;
     ///
     /// #[derive(Deserialize)]
     /// struct SomeStruct {
@@ -2432,7 +2505,7 @@ where
     /// let err = SomeStruct::deserialize(&mut de);
     /// assert!(err.is_err());
     ///
-    /// let reader: &Reader<_> = de.get_ref().get_ref();
+    /// let reader: &NsReader<_> = de.get_ref().get_ref();
     ///
     /// assert_eq!(reader.error_position(), 28);
     /// assert_eq!(reader.buffer_position(), 41);
@@ -2534,6 +2607,22 @@ where
         }
     }
 
+    #[inline]
+    fn last_peeked(&self) -> &DeEvent<'de> {
+        #[cfg(feature = "overlapped-lists")]
+        {
+            self.read
+                .front()
+                .expect("`Deserializer::peek()` should be called")
+        }
+        #[cfg(not(feature = "overlapped-lists"))]
+        {
+            self.peek
+                .as_ref()
+                .expect("`Deserializer::peek()` should be called")
+        }
+    }
+
     fn next(&mut self) -> Result<DeEvent<'de>, DeError> {
         // Replay skipped or peeked events
         #[cfg(feature = "overlapped-lists")]
@@ -2764,6 +2853,14 @@ where
         }
         self.reader.read_to_end(name)
     }
+
+    fn skip_next_tree(&mut self) -> Result<(), DeError> {
+        let DeEvent::Start(start) = self.next()? else {
+            unreachable!("Only call this if the next event is a start event")
+        };
+        let name = start.name();
+        self.read_to_end(name)
+    }
 }
 
 impl<'de> Deserializer<'de, SliceReader<'de>> {
@@ -2783,7 +2880,7 @@ where
     /// Create new deserializer that will borrow data from the specified string
     /// and use specified entity resolver.
     pub fn from_str_with_resolver(source: &'de str, entity_resolver: E) -> Self {
-        let mut reader = Reader::from_str(source);
+        let mut reader = NsReader::from_str(source);
         let config = reader.config_mut();
         config.expand_empty_elements = true;
 
@@ -2826,7 +2923,7 @@ where
     /// will borrow instead of copy. If you have `&[u8]` which is known to represent
     /// UTF-8, you can decode it first before using [`from_str`].
     pub fn with_resolver(reader: R, entity_resolver: E) -> Self {
-        let mut reader = Reader::from_reader(reader);
+        let mut reader = NsReader::from_reader(reader);
         let config = reader.config_mut();
         config.expand_empty_elements = true;
 
@@ -2945,9 +3042,16 @@ where
     where
         V: Visitor<'de>,
     {
-        match self.peek()? {
+        // We cannot use result of `peek()` directly because of borrow checker
+        let _ = self.peek()?;
+        match self.last_peeked() {
             DeEvent::Text(t) if t.is_empty() => visitor.visit_none(),
             DeEvent::Eof => visitor.visit_none(),
+            // if the `xsi:nil` attribute is set to true we got a none value
+            DeEvent::Start(start) if self.reader.reader.has_nil_attr(&start) => {
+                self.skip_next_tree()?;
+                visitor.visit_none()
+            }
             _ => visitor.visit_some(self),
         }
     }
@@ -3071,6 +3175,12 @@ pub trait XmlRead<'i> {
 
     /// A copy of the reader's decoder used to decode strings.
     fn decoder(&self) -> Decoder;
+
+    /// Checks if the `start` tag has a [`xsi:nil`] attribute. This method ignores
+    /// any errors in attributes.
+    ///
+    /// [`xsi:nil`]: https://www.w3.org/TR/xmlschema-1/#xsi_nil
+    fn has_nil_attr(&self, start: &BytesStart) -> bool;
 }
 
 /// XML input source that reads from a std::io input stream.
@@ -3078,7 +3188,7 @@ pub trait XmlRead<'i> {
 /// You cannot create it, it is created automatically when you call
 /// [`Deserializer::from_reader`]
 pub struct IoReader<R: BufRead> {
-    reader: Reader<R>,
+    reader: NsReader<R>,
     start_trimmer: StartTrimmer,
     buf: Vec<u8>,
 }
@@ -3091,7 +3201,7 @@ impl<R: BufRead> IoReader<R> {
     /// use serde::Deserialize;
     /// use std::io::Cursor;
     /// use quick_xml::de::Deserializer;
-    /// use quick_xml::Reader;
+    /// use quick_xml::NsReader;
     ///
     /// #[derive(Deserialize)]
     /// struct SomeStruct {
@@ -3108,12 +3218,12 @@ impl<R: BufRead> IoReader<R> {
     /// let err = SomeStruct::deserialize(&mut de);
     /// assert!(err.is_err());
     ///
-    /// let reader: &Reader<Cursor<&str>> = de.get_ref().get_ref();
+    /// let reader: &NsReader<Cursor<&str>> = de.get_ref().get_ref();
     ///
     /// assert_eq!(reader.error_position(), 28);
     /// assert_eq!(reader.buffer_position(), 41);
     /// ```
-    pub const fn get_ref(&self) -> &Reader<R> {
+    pub const fn get_ref(&self) -> &NsReader<R> {
         &self.reader
     }
 }
@@ -3140,6 +3250,10 @@ impl<'i, R: BufRead> XmlRead<'i> for IoReader<R> {
     fn decoder(&self) -> Decoder {
         self.reader.decoder()
     }
+
+    fn has_nil_attr(&self, start: &BytesStart) -> bool {
+        start.attributes().has_nil(&self.reader)
+    }
 }
 
 /// XML input source that reads from a slice of bytes and can borrow from it.
@@ -3147,7 +3261,7 @@ impl<'i, R: BufRead> XmlRead<'i> for IoReader<R> {
 /// You cannot create it, it is created automatically when you call
 /// [`Deserializer::from_str`].
 pub struct SliceReader<'de> {
-    reader: Reader<&'de [u8]>,
+    reader: NsReader<&'de [u8]>,
     start_trimmer: StartTrimmer,
 }
 
@@ -3158,7 +3272,7 @@ impl<'de> SliceReader<'de> {
     /// # use pretty_assertions::assert_eq;
     /// use serde::Deserialize;
     /// use quick_xml::de::Deserializer;
-    /// use quick_xml::Reader;
+    /// use quick_xml::NsReader;
     ///
     /// #[derive(Deserialize)]
     /// struct SomeStruct {
@@ -3175,12 +3289,12 @@ impl<'de> SliceReader<'de> {
     /// let err = SomeStruct::deserialize(&mut de);
     /// assert!(err.is_err());
     ///
-    /// let reader: &Reader<&[u8]> = de.get_ref().get_ref();
+    /// let reader: &NsReader<&[u8]> = de.get_ref().get_ref();
     ///
     /// assert_eq!(reader.error_position(), 28);
     /// assert_eq!(reader.buffer_position(), 41);
     /// ```
-    pub const fn get_ref(&self) -> &Reader<&'de [u8]> {
+    pub const fn get_ref(&self) -> &NsReader<&'de [u8]> {
         &self.reader
     }
 }
@@ -3205,6 +3319,10 @@ impl<'de> XmlRead<'de> for SliceReader<'de> {
     fn decoder(&self) -> Decoder {
         self.reader.decoder()
     }
+
+    fn has_nil_attr(&self, start: &BytesStart) -> bool {
+        start.attributes().has_nil(&self.reader)
+    }
 }
 
 #[cfg(test)]
@@ -3781,12 +3899,12 @@ mod tests {
         "#;
 
         let mut reader1 = IoReader {
-            reader: Reader::from_reader(s.as_bytes()),
+            reader: NsReader::from_reader(s.as_bytes()),
             start_trimmer: StartTrimmer::default(),
             buf: Vec::new(),
         };
         let mut reader2 = SliceReader {
-            reader: Reader::from_str(s),
+            reader: NsReader::from_str(s),
             start_trimmer: StartTrimmer::default(),
         };
 
@@ -3812,7 +3930,7 @@ mod tests {
         "#;
 
         let mut reader = SliceReader {
-            reader: Reader::from_str(s),
+            reader: NsReader::from_str(s),
             start_trimmer: StartTrimmer::default(),
         };
 
diff --git a/src/events/attributes.rs b/src/events/attributes.rs
index dd35c4d..c5be1b3 100644
--- a/src/events/attributes.rs
+++ b/src/events/attributes.rs
@@ -5,8 +5,9 @@
 use crate::encoding::Decoder;
 use crate::errors::Result as XmlResult;
 use crate::escape::{escape, resolve_predefined_entity, unescape_with};
-use crate::name::QName;
-use crate::utils::{is_whitespace, write_byte_string, write_cow_string, Bytes};
+use crate::name::{LocalName, Namespace, QName};
+use crate::reader::NsReader;
+use crate::utils::{is_whitespace, Bytes};
 
 use std::fmt::{self, Debug, Display, Formatter};
 use std::iter::FusedIterator;
@@ -96,15 +97,50 @@ impl<'a> Attribute<'a> {
             Cow::Owned(s) => Ok(s.into()),
         }
     }
+
+    /// If attribute value [represents] valid boolean values, returns `Some`, otherwise returns `None`.
+    ///
+    /// The valid boolean representations are only `"true"`, `"false"`, `"1"`, and `"0"`.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use pretty_assertions::assert_eq;
+    /// use quick_xml::events::attributes::Attribute;
+    ///
+    /// let attr = Attribute::from(("attr", "false"));
+    /// assert_eq!(attr.as_bool(), Some(false));
+    ///
+    /// let attr = Attribute::from(("attr", "0"));
+    /// assert_eq!(attr.as_bool(), Some(false));
+    ///
+    /// let attr = Attribute::from(("attr", "true"));
+    /// assert_eq!(attr.as_bool(), Some(true));
+    ///
+    /// let attr = Attribute::from(("attr", "1"));
+    /// assert_eq!(attr.as_bool(), Some(true));
+    ///
+    /// let attr = Attribute::from(("attr", "bot bool"));
+    /// assert_eq!(attr.as_bool(), None);
+    /// ```
+    ///
+    /// [represents]: https://www.w3.org/TR/xmlschema11-2/#boolean
+    #[inline]
+    pub fn as_bool(&self) -> Option<bool> {
+        match self.value.as_ref() {
+            b"1" | b"true" => Some(true),
+            b"0" | b"false" => Some(false),
+            _ => None,
+        }
+    }
 }
 
 impl<'a> Debug for Attribute<'a> {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        write!(f, "Attribute {{ key: ")?;
-        write_byte_string(f, self.key.as_ref())?;
-        write!(f, ", value: ")?;
-        write_cow_string(f, &self.value)?;
-        write!(f, " }}")
+        f.debug_struct("Attribute")
+            .field("key", &Bytes(self.key.as_ref()))
+            .field("value", &Bytes(&self.value))
+            .finish()
     }
 }
 
@@ -196,7 +232,7 @@ impl<'a> From<Attr<&'a [u8]>> for Attribute<'a> {
 /// The duplicate check can be turned off by calling [`with_checks(false)`].
 ///
 /// [`with_checks(false)`]: Self::with_checks
-#[derive(Clone, Debug)]
+#[derive(Clone)]
 pub struct Attributes<'a> {
     /// Slice of `BytesStart` corresponding to attributes
     bytes: &'a [u8],
@@ -234,6 +270,90 @@ impl<'a> Attributes<'a> {
         self.state.check_duplicates = val;
         self
     }
+
+    /// Checks if the current tag has a [`xsi:nil`] attribute. This method ignores any errors in
+    /// attributes.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use pretty_assertions::assert_eq;
+    /// use quick_xml::events::Event;
+    /// use quick_xml::name::QName;
+    /// use quick_xml::reader::NsReader;
+    ///
+    /// let mut reader = NsReader::from_str("
+    ///     <root xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
+    ///         <true xsi:nil='true'/>
+    ///         <false xsi:nil='false'/>
+    ///         <none/>
+    ///         <non-xsi xsi:nil='true' xmlns:xsi='namespace'/>
+    ///         <unbound-nil nil='true' xmlns='http://www.w3.org/2001/XMLSchema-instance'/>
+    ///         <another-xmlns f:nil='true' xmlns:f='http://www.w3.org/2001/XMLSchema-instance'/>
+    ///     </root>
+    /// ");
+    /// reader.config_mut().trim_text(true);
+    ///
+    /// macro_rules! check {
+    ///     ($reader:expr, $name:literal, $value:literal) => {
+    ///         let event = match $reader.read_event().unwrap() {
+    ///             Event::Empty(e) => e,
+    ///             e => panic!("Unexpected event {:?}", e),
+    ///         };
+    ///         assert_eq!(
+    ///             (event.name(), event.attributes().has_nil(&$reader)),
+    ///             (QName($name.as_bytes()), $value),
+    ///         );
+    ///     };
+    /// }
+    ///
+    /// let root = match reader.read_event().unwrap() {
+    ///     Event::Start(e) => e,
+    ///     e => panic!("Unexpected event {:?}", e),
+    /// };
+    /// assert_eq!(root.attributes().has_nil(&reader), false);
+    ///
+    /// // definitely true
+    /// check!(reader, "true",          true);
+    /// // definitely false
+    /// check!(reader, "false",         false);
+    /// // absence of the attribute means that attribute is not set
+    /// check!(reader, "none",          false);
+    /// // attribute not bound to the correct namespace
+    /// check!(reader, "non-xsi",       false);
+    /// // attributes without prefix not bound to any namespace
+    /// check!(reader, "unbound-nil",   false);
+    /// // prefix can be any while it is bound to the correct namespace
+    /// check!(reader, "another-xmlns", true);
+    /// ```
+    ///
+    /// [`xsi:nil`]: https://www.w3.org/TR/xmlschema-1/#xsi_nil
+    pub fn has_nil<R>(&mut self, reader: &NsReader<R>) -> bool {
+        use crate::name::ResolveResult::*;
+
+        self.any(|attr| {
+            if let Ok(attr) = attr {
+                match reader.resolve_attribute(attr.key) {
+                    (
+                        Bound(Namespace(b"http://www.w3.org/2001/XMLSchema-instance")),
+                        LocalName(b"nil"),
+                    ) => attr.as_bool().unwrap_or_default(),
+                    _ => false,
+                }
+            } else {
+                false
+            }
+        })
+    }
+}
+
+impl<'a> Debug for Attributes<'a> {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        f.debug_struct("Attributes")
+            .field("bytes", &Bytes(&self.bytes))
+            .field("state", &self.state)
+            .finish()
+    }
 }
 
 impl<'a> Iterator for Attributes<'a> {
diff --git a/src/name.rs b/src/name.rs
index b7a8de1..6bed83a 100644
--- a/src/name.rs
+++ b/src/name.rs
@@ -200,7 +200,7 @@ impl<'a> AsRef<[u8]> for QName<'a> {
 /// [local (unqualified) name]: https://www.w3.org/TR/xml-names11/#dt-localname
 #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "serde-types", derive(serde::Deserialize, serde::Serialize))]
-pub struct LocalName<'a>(&'a [u8]);
+pub struct LocalName<'a>(pub(crate) &'a [u8]);
 impl<'a> LocalName<'a> {
     /// Converts this name to an internal slice representation.
     #[inline(always)]


Test patch:
diff --git a/tests/serde-de-xsi.rs b/tests/serde-de-xsi.rs
new file mode 100644
index 0000000..4db9e60
--- /dev/null
+++ b/tests/serde-de-xsi.rs
@@ -0,0 +1,704 @@
+//! Tests for ensure behavior of `xsi:nil` handling.
+//!
+//! We want to threat element with `xsi:nil="true"` as `None` in optional contexts.
+use quick_xml::se::to_string;
+use quick_xml::DeError;
+
+use serde::{Deserialize, Serialize};
+
+mod serde_helpers;
+use serde_helpers::from_str;
+
+#[derive(Debug, Deserialize, PartialEq, Serialize)]
+struct Foo {
+    elem: String,
+}
+
+macro_rules! assert_error_matches {
+    ($res: expr, $err: pat) => {
+        assert!(
+            matches!($res, Err($err)),
+            concat!("Expected `", stringify!($err), "`, but got `{:?}`"),
+            $res
+        );
+    };
+}
+
+mod top_level_option {
+    use super::*;
+
+    mod empty {
+        use super::*;
+
+        /// Without `xsi:nil="true"` tags in optional contexts are always considered as having
+        /// `Some` value, but because we do not have `tag` element, deserialization failed
+        #[test]
+        fn none() {
+            let xml = r#"<foo/>"#;
+            assert_error_matches!(from_str::<Option<Foo>>(xml), DeError::Custom(_));
+        }
+
+        /// When prefix is not defined, attributes not bound to any namespace (unlike elements),
+        /// so just `nil="true"` does not mean that `xsi:nil` is set
+        mod no_prefix {
+            use super::*;
+
+            #[test]
+            fn true_() {
+                let xml = r#"<foo xmlns="http://www.w3.org/2001/XMLSchema-instance" nil="true"/>"#;
+                assert_error_matches!(from_str::<Option<Foo>>(xml), DeError::Custom(_));
+            }
+
+            #[test]
+            fn false_() {
+                let xml = r#"<foo xmlns="http://www.w3.org/2001/XMLSchema-instance" nil="false"/>"#;
+                assert_error_matches!(from_str::<Option<Foo>>(xml), DeError::Custom(_));
+            }
+        }
+
+        /// Check canonical prefix
+        mod xsi {
+            use super::*;
+            use pretty_assertions::assert_eq;
+
+            #[test]
+            fn true_() {
+                let xml = r#"<foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>"#;
+                assert_eq!(from_str::<Option<Foo>>(xml).unwrap(), None);
+            }
+
+            #[test]
+            fn false_() {
+                let xml = r#"<foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="false"/>"#;
+                assert_error_matches!(from_str::<Option<Foo>>(xml), DeError::Custom(_));
+            }
+        }
+
+        /// Check other prefix to be sure that we not process only canonical prefixes
+        mod ns0 {
+            use super::*;
+            use pretty_assertions::assert_eq;
+
+            #[test]
+            fn true_() {
+                let xml = r#"<foo xmlns:ns0="http://www.w3.org/2001/XMLSchema-instance" ns0:nil="true"/>"#;
+                assert_eq!(from_str::<Option<Foo>>(xml).unwrap(), None);
+            }
+
+            #[test]
+            fn false_() {
+                let xml = r#"<foo xmlns:ns0="http://www.w3.org/2001/XMLSchema-instance" ns0:nil="false"/>"#;
+                assert_error_matches!(from_str::<Option<Foo>>(xml), DeError::Custom(_));
+            }
+        }
+    }
+
+    /// We have no place to store attribute of the element, so the behavior must be the same
+    /// as without attributes.
+    mod with_attr {
+        use super::*;
+
+        #[test]
+        fn none() {
+            let xml = r#"<foo attr="value"/>"#;
+            assert_error_matches!(from_str::<Option<Foo>>(xml), DeError::Custom(_));
+        }
+
+        /// When prefix is not defined, attributes not bound to any namespace (unlike elements),
+        /// so just `nil="true"` does not mean that `xsi:nil` is set
+        mod no_prefix {
+            use super::*;
+
+            #[test]
+            fn true_() {
+                let xml = r#"<foo xmlns="http://www.w3.org/2001/XMLSchema-instance" nil="true" attr="value"/>"#;
+                assert_error_matches!(from_str::<Option<Foo>>(xml), DeError::Custom(_));
+            }
+
+            #[test]
+            fn false_() {
+                let xml = r#"<foo xmlns="http://www.w3.org/2001/XMLSchema-instance" nil="false" attr="value"/>"#;
+                assert_error_matches!(from_str::<Option<Foo>>(xml), DeError::Custom(_));
+            }
+        }
+
+        /// Check canonical prefix
+        mod xsi {
+            use super::*;
+            use pretty_assertions::assert_eq;
+
+            #[test]
+            fn true_() {
+                let xml = r#"<foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true" attr="value"/>"#;
+                assert_eq!(from_str::<Option<Foo>>(xml).unwrap(), None);
+            }
+
+            #[test]
+            fn false_() {
+                let xml = r#"<foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="false" attr="value"/>"#;
+                assert_error_matches!(from_str::<Option<Foo>>(xml), DeError::Custom(_));
+            }
+        }
+
+        /// Check other prefix to be sure that we not process only canonical prefixes
+        mod ns0 {
+            use super::*;
+            use pretty_assertions::assert_eq;
+
+            #[test]
+            fn true_() {
+                let xml = r#"<foo xmlns:ns0="http://www.w3.org/2001/XMLSchema-instance" ns0:nil="true" attr="value"/>"#;
+                assert_eq!(from_str::<Option<Foo>>(xml).unwrap(), None);
+            }
+
+            #[test]
+            fn false_() {
+                let xml = r#"<foo xmlns:ns0="http://www.w3.org/2001/XMLSchema-instance" ns0:nil="false" attr="value"/>"#;
+                assert_error_matches!(from_str::<Option<Foo>>(xml), DeError::Custom(_));
+            }
+        }
+    }
+
+    mod with_element {
+        use super::*;
+        use pretty_assertions::assert_eq;
+
+        #[test]
+        fn none() {
+            let xml = r#"<foo><elem>Foo</elem></foo>"#;
+            assert_eq!(
+                from_str::<Option<Foo>>(xml).unwrap(),
+                Some(Foo { elem: "Foo".into() })
+            );
+        }
+
+        /// When prefix is not defined, attributes not bound to any namespace (unlike elements),
+        /// so just `nil="true"` does not mean that `xsi:nil` is set
+        mod no_prefix {
+            use super::*;
+            use pretty_assertions::assert_eq;
+
+            #[test]
+            fn true_() {
+                let xml = r#"<foo xmlns="http://www.w3.org/2001/XMLSchema-instance" nil="true"><elem>Foo</elem></foo>"#;
+                assert_eq!(
+                    from_str::<Option<Foo>>(xml).unwrap(),
+                    Some(Foo { elem: "Foo".into() })
+                );
+            }
+
+            #[test]
+            fn false_() {
+                let xml = r#"<foo xmlns="http://www.w3.org/2001/XMLSchema-instance" nil="false"><elem>Foo</elem></foo>"#;
+                assert_eq!(
+                    from_str::<Option<Foo>>(xml).unwrap(),
+                    Some(Foo { elem: "Foo".into() })
+                );
+            }
+        }
+
+        /// Check canonical prefix
+        mod xsi {
+            use super::*;
+            use pretty_assertions::assert_eq;
+
+            #[test]
+            fn true_() {
+                let xml = r#"<foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"><elem>Foo</elem></foo>"#;
+                assert_eq!(from_str::<Option<Foo>>(xml).unwrap(), None);
+            }
+
+            #[test]
+            fn false_() {
+                let xml = r#"<foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="false"><elem>Foo</elem></foo>"#;
+                assert_eq!(
+                    from_str::<Option<Foo>>(xml).unwrap(),
+                    Some(Foo { elem: "Foo".into() })
+                );
+            }
+        }
+
+        /// Check other prefix to be sure that we not process only canonical prefixes
+        mod ns0 {
+            use super::*;
+            use pretty_assertions::assert_eq;
+
+            #[test]
+            fn true_() {
+                let xml = r#"<foo xmlns:ns0="http://www.w3.org/2001/XMLSchema-instance" ns0:nil="true"><elem>Foo</elem></foo>"#;
+                assert_eq!(from_str::<Option<Foo>>(xml).unwrap(), None);
+            }
+
+            #[test]
+            fn false_() {
+                let xml = r#"<foo xmlns:ns0="http://www.w3.org/2001/XMLSchema-instance" ns0:nil="false"><elem>Foo</elem></foo>"#;
+                assert_eq!(
+                    from_str::<Option<Foo>>(xml).unwrap(),
+                    Some(Foo { elem: "Foo".into() })
+                );
+            }
+        }
+    }
+}
+
+mod as_field {
+    use super::*;
+
+    /// According to the [specification], `xsi:nil` controls only ability to (not) have nested
+    /// elements, but it does not applied to attributes. Due to that we ensure, that attributes
+    /// are still can be accessed.
+    ///
+    /// [specification]: https://www.w3.org/TR/xmlschema11-1/#Instance_Document_Constructions
+    #[derive(Debug, Deserialize, PartialEq, Serialize)]
+    struct AnyName {
+        #[serde(rename = "@attr")]
+        attr: Option<String>,
+
+        elem: Option<String>,
+    }
+
+    #[derive(Debug, Deserialize, PartialEq)]
+    struct Root {
+        foo: AnyName,
+    }
+
+    #[derive(Debug, Deserialize, PartialEq)]
+    struct Bar {
+        foo: Option<Foo>,
+    }
+
+    macro_rules! check {
+        (
+            $name:ident,
+            $true_xml:literal,
+            $false_xml:literal,
+            $se_xml:literal,
+            $attr:expr,
+        ) => {
+            mod $name {
+                use super::*;
+                use pretty_assertions::assert_eq;
+
+                #[test]
+                fn true_() {
+                    let value = AnyName {
+                        attr: $attr,
+                        elem: None,
+                    };
+
+                    assert_eq!(to_string(&value).unwrap(), $se_xml);
+                    assert_eq!(from_str::<AnyName>($true_xml).unwrap(), value);
+                }
+
+                #[test]
+                fn false_() {
+                    let value = AnyName {
+                        attr: $attr,
+                        elem: None,
+                    };
+
+                    assert_eq!(to_string(&value).unwrap(), $se_xml);
+                    assert_eq!(from_str::<AnyName>($false_xml).unwrap(), value);
+                }
+            }
+        };
+    }
+
+    mod empty {
+        use super::*;
+        use pretty_assertions::assert_eq;
+
+        /// Without `xsi:nil="true"` tags in optional contexts are always considered as having
+        /// `Some` value, but because we do not have `tag` element, deserialization failed
+        #[test]
+        fn none() {
+            let value = AnyName {
+                attr: None,
+                elem: None,
+            };
+
+            assert_eq!(
+                to_string(&value).unwrap(),
+                r#"<AnyName attr=""><elem/></AnyName>"#
+            );
+            assert_eq!(
+                from_str::<AnyName>("<AnyName/>").unwrap(),
+                AnyName {
+                    attr: None,
+                    elem: None,
+                }
+            );
+        }
+
+        // When prefix is not defined, attributes not bound to any namespace (unlike elements),
+        // so just `nil="true"` does not mean that `xsi:nil` is set. But because `AnyName` is empty
+        // there anyway nothing inside, so all fields will be set to `None`
+        check!(
+            no_prefix,
+            r#"<AnyName xmlns="http://www.w3.org/2001/XMLSchema-instance" nil="true"/>"#,
+            r#"<AnyName xmlns="http://www.w3.org/2001/XMLSchema-instance" nil="false"/>"#,
+            r#"<AnyName attr=""><elem/></AnyName>"#,
+            None,
+        );
+
+        // Check canonical prefix
+        check!(
+            xsi,
+            r#"<AnyName xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>"#,
+            r#"<AnyName xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="false"/>"#,
+            r#"<AnyName attr=""><elem/></AnyName>"#,
+            None,
+        );
+
+        // Check other prefix to be sure that we do not process only canonical prefixes
+        check!(
+            ns0,
+            r#"<AnyName xmlns:ns0="http://www.w3.org/2001/XMLSchema-instance" ns0:nil="true"/>"#,
+            r#"<AnyName xmlns:ns0="http://www.w3.org/2001/XMLSchema-instance" ns0:nil="false"/>"#,
+            r#"<AnyName attr=""><elem/></AnyName>"#,
+            None,
+        );
+
+        mod nested {
+            use super::*;
+            use pretty_assertions::assert_eq;
+
+            #[test]
+            fn none() {
+                let xml =
+                    r#"<bar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><foo/></bar>"#;
+
+                assert_error_matches!(from_str::<Bar>(xml), DeError::Custom(_));
+                assert_eq!(
+                    from_str::<Root>(xml).unwrap(),
+                    Root {
+                        foo: AnyName {
+                            attr: None,
+                            elem: None,
+                        },
+                    }
+                );
+            }
+
+            #[test]
+            fn true_() {
+                let xml = r#"<bar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><foo xsi:nil="true"/></bar>"#;
+
+                assert_eq!(from_str::<Bar>(xml).unwrap(), Bar { foo: None });
+                assert_eq!(
+                    from_str::<Root>(xml).unwrap(),
+                    Root {
+                        foo: AnyName {
+                            attr: None,
+                            elem: None,
+                        },
+                    }
+                );
+            }
+
+            #[test]
+            fn false_() {
+                let xml = r#"<bar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><foo xsi:nil="false"/></bar>"#;
+
+                assert_error_matches!(from_str::<Bar>(xml), DeError::Custom(_));
+                assert_eq!(
+                    from_str::<Root>(xml).unwrap(),
+                    Root {
+                        foo: AnyName {
+                            attr: None,
+                            elem: None,
+                        },
+                    }
+                );
+            }
+        }
+    }
+
+    /// We have no place to store attribute of the element, so the behavior must be the same
+    /// as without attributes.
+    mod with_attr {
+        use super::*;
+        use pretty_assertions::assert_eq;
+
+        #[test]
+        fn none() {
+            let value = AnyName {
+                attr: Some("value".into()),
+                elem: None,
+            };
+
+            assert_eq!(
+                to_string(&value).unwrap(),
+                r#"<AnyName attr="value"><elem/></AnyName>"#
+            );
+            assert_eq!(
+                from_str::<AnyName>(r#"<AnyName attr="value"/>"#).unwrap(),
+                value
+            );
+        }
+
+        // When prefix is not defined, attributes not bound to any namespace (unlike elements),
+        // so just `nil="true"` does not mean that `xsi:nil` is set. But because `AnyName` is empty
+        // there anyway nothing inside, so all element fields will be set to `None`
+        check!(
+            no_prefix,
+            r#"<AnyName xmlns="http://www.w3.org/2001/XMLSchema-instance" nil="true" attr="value"/>"#,
+            r#"<AnyName xmlns="http://www.w3.org/2001/XMLSchema-instance" nil="false" attr="value"/>"#,
+            r#"<AnyName attr="value"><elem/></AnyName>"#,
+            Some("value".into()),
+        );
+
+        // Check canonical prefix
+        check!(
+            xsi,
+            r#"<AnyName xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true" attr="value"/>"#,
+            r#"<AnyName xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="false" attr="value"/>"#,
+            r#"<AnyName attr="value"><elem/></AnyName>"#,
+            Some("value".into()),
+        );
+
+        // Check other prefix to be sure that we do not process only canonical prefixes
+        check!(
+            ns0,
+            r#"<AnyName xmlns:ns0="http://www.w3.org/2001/XMLSchema-instance" ns0:nil="true" attr="value"/>"#,
+            r#"<AnyName xmlns:ns0="http://www.w3.org/2001/XMLSchema-instance" ns0:nil="false" attr="value"/>"#,
+            r#"<AnyName attr="value"><elem/></AnyName>"#,
+            Some("value".into()),
+        );
+
+        mod nested {
+            use super::*;
+            use pretty_assertions::assert_eq;
+
+            #[test]
+            fn none() {
+                let xml = r#"<bar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><foo attr="value"/></bar>"#;
+
+                // Without `xsi:nil="true"` <foo> is mapped to `foo` field,
+                // but failed to deserialzie because of missing required <elem> tag
+                assert_error_matches!(from_str::<Bar>(xml), DeError::Custom(_));
+                assert_eq!(
+                    from_str::<Root>(xml).unwrap(),
+                    Root {
+                        foo: AnyName {
+                            attr: Some("value".into()),
+                            elem: None,
+                        },
+                    }
+                );
+            }
+
+            #[test]
+            fn true_() {
+                let xml = r#"<bar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><foo xsi:nil="true" attr="value"/></bar>"#;
+
+                assert_eq!(from_str::<Bar>(xml).unwrap(), Bar { foo: None });
+                assert_eq!(
+                    from_str::<Root>(xml).unwrap(),
+                    Root {
+                        foo: AnyName {
+                            attr: Some("value".into()),
+                            elem: None,
+                        },
+                    }
+                );
+            }
+
+            #[test]
+            fn false_() {
+                let xml = r#"<bar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><foo xsi:nil="false" attr="value"/></bar>"#;
+
+                // With `xsi:nil="false"` <foo> is mapped to `foo` field,
+                // but failed to deserialzie because of missing required <elem> tag
+                assert_error_matches!(from_str::<Bar>(xml), DeError::Custom(_));
+                assert_eq!(
+                    from_str::<Root>(xml).unwrap(),
+                    Root {
+                        foo: AnyName {
+                            attr: Some("value".into()),
+                            elem: None,
+                        },
+                    }
+                );
+            }
+        }
+    }
+
+    mod with_element {
+        use super::*;
+        use pretty_assertions::assert_eq;
+
+        macro_rules! check {
+            (
+                $name:ident,
+
+                $de_true_xml:literal,
+                $se_true_xml:literal,
+
+                $de_false_xml:literal,
+                $se_false_xml:literal,
+            ) => {
+                mod $name {
+                    use super::*;
+                    use pretty_assertions::assert_eq;
+
+                    #[test]
+                    fn true_() {
+                        let value = AnyName {
+                            attr: None,
+                            // Becase `nil=true``, element deserialized as `None`
+                            elem: None,
+                        };
+
+                        assert_eq!(to_string(&value).unwrap(), $se_true_xml);
+                        assert_eq!(from_str::<AnyName>($de_true_xml).unwrap(), value);
+                    }
+
+                    #[test]
+                    fn false_() {
+                        let value = AnyName {
+                            attr: None,
+                            elem: Some("Foo".into()),
+                        };
+
+                        assert_eq!(to_string(&value).unwrap(), $se_false_xml);
+                        assert_eq!(from_str::<AnyName>($de_false_xml).unwrap(), value);
+                    }
+                }
+            };
+        }
+
+        #[test]
+        fn none() {
+            let value = AnyName {
+                attr: None,
+                elem: Some("Foo".into()),
+            };
+
+            assert_eq!(
+                to_string(&value).unwrap(),
+                r#"<AnyName attr=""><elem>Foo</elem></AnyName>"#
+            );
+            assert_eq!(
+                from_str::<AnyName>(r#"<AnyName><elem>Foo</elem></AnyName>"#).unwrap(),
+                value
+            );
+        }
+
+        /// When prefix is not defined, attributes not bound to any namespace (unlike elements),
+        /// so just `nil="true"` does not mean that `xsi:nil` is set
+        mod no_prefix {
+            use super::*;
+            use pretty_assertions::assert_eq;
+
+            #[test]
+            fn true_() {
+                let se_xml = r#"<AnyName attr=""><elem>Foo</elem></AnyName>"#;
+                let de_xml = r#"<AnyName xmlns="http://www.w3.org/2001/XMLSchema-instance" nil="true"><elem>Foo</elem></AnyName>"#;
+
+                let value = AnyName {
+                    attr: None,
+                    elem: Some("Foo".into()),
+                };
+
+                assert_eq!(to_string(&value).unwrap(), se_xml);
+                assert_eq!(from_str::<AnyName>(de_xml).unwrap(), value);
+            }
+
+            #[test]
+            fn false_() {
+                let se_xml = r#"<AnyName attr=""><elem>Foo</elem></AnyName>"#;
+                let de_xml = r#"<AnyName xmlns="http://www.w3.org/2001/XMLSchema-instance" nil="false"><elem>Foo</elem></AnyName>"#;
+
+                let value = AnyName {
+                    attr: None,
+                    elem: Some("Foo".into()),
+                };
+
+                assert_eq!(to_string(&value).unwrap(), se_xml);
+                assert_eq!(from_str::<AnyName>(de_xml).unwrap(), value);
+            }
+        }
+
+        // Check canonical prefix
+        check!(
+            xsi,
+            r#"<AnyName xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"><elem>Foo</elem></AnyName>"#,
+            r#"<AnyName attr=""><elem/></AnyName>"#,
+            r#"<AnyName xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="false"><elem>Foo</elem></AnyName>"#,
+            r#"<AnyName attr=""><elem>Foo</elem></AnyName>"#,
+        );
+
+        // Check other prefix to be sure that we do not process only canonical prefixes
+        check!(
+            ns0,
+            r#"<AnyName xmlns:ns0="http://www.w3.org/2001/XMLSchema-instance" ns0:nil="true"><elem>Foo</elem></AnyName>"#,
+            r#"<AnyName attr=""><elem/></AnyName>"#,
+            r#"<AnyName xmlns:ns0="http://www.w3.org/2001/XMLSchema-instance" ns0:nil="false"><elem>Foo</elem></AnyName>"#,
+            r#"<AnyName attr=""><elem>Foo</elem></AnyName>"#,
+        );
+
+        mod nested {
+            use super::*;
+            use pretty_assertions::assert_eq;
+
+            #[test]
+            fn none() {
+                let xml = r#"<bar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><foo><elem>Foo</elem></foo></bar>"#;
+
+                assert_eq!(
+                    from_str::<Bar>(xml).unwrap(),
+                    Bar {
+                        foo: Some(Foo { elem: "Foo".into() }),
+                    }
+                );
+                assert_eq!(
+                    from_str::<Root>(xml).unwrap(),
+                    Root {
+                        foo: AnyName {
+                            attr: None,
+                            elem: Some("Foo".into()),
+                        },
+                    }
+                );
+            }
+
+            #[test]
+            fn true_() {
+                let xml = r#"<bar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><foo xsi:nil="true"><elem>Foo</elem></foo></bar>"#;
+
+                assert_eq!(from_str::<Bar>(xml).unwrap(), Bar { foo: None });
+                assert_eq!(
+                    from_str::<Root>(xml).unwrap(),
+                    Root {
+                        foo: AnyName {
+                            attr: None,
+                            elem: None,
+                        },
+                    }
+                );
+            }
+
+            #[test]
+            fn false_() {
+                let xml = r#"<bar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><foo xsi:nil="false"><elem>Foo</elem></foo></bar>"#;
+
+                assert_eq!(
+                    from_str::<Bar>(xml).unwrap(),
+                    Bar {
+                        foo: Some(Foo { elem: "Foo".into() }),
+                    }
+                );
+                assert_eq!(
+                    from_str::<Root>(xml).unwrap(),
+                    Root {
+                        foo: AnyName {
+                            attr: None,
+                            elem: Some("Foo".into()),
+                        },
+                    }
+                );
+            }
+        }
+    }
+}
diff --git a/tests/serde-issues.rs b/tests/serde-issues.rs
index bac7d41..7826669 100644
--- a/tests/serde-issues.rs
+++ b/tests/serde-issues.rs
@@ -218,6 +218,23 @@ fn issue429() {
     );
 }
 
+/// Regression test for https://github.com/tafia/quick-xml/issues/497.
+#[test]
+fn issue497() {
+    #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
+    struct Player {
+        #[serde(skip_serializing_if = "Option::is_none")]
+        spawn_forced: Option<bool>,
+    }
+    let data = Player { spawn_forced: None };
+
+    let deserialize_buffer = to_string(&data).unwrap();
+    dbg!(&deserialize_buffer);
+
+    let p: Player = from_reader(deserialize_buffer.as_bytes()).unwrap();
+    assert_eq!(p, data);
+}
+
 /// Regression test for https://github.com/tafia/quick-xml/issues/500.
 #[test]
 fn issue500() {