Repository: tafia/quick-xml
Pull request #172 (2019-11-25 15:37:01)

WIP Serde
Add a `serialize` feature to allow (de)serializing xmls.

This is hugely inspired by [serde-xml](https://github.com/RReverser/serde-xml-rs) so most of the credit goes to them.

For the moment only Deserialization has been worked on. Serialization should be simpler.

In terms of performance, there is probably some optimization to do in the future. In particular we should be able to avoid the `'static` lifetime in `Deserializer::next` (thus avoiding some unecessary allocations/copy).


Patch:
diff --git a/.travis.yml b/.travis.yml
index 28e7a36..8600298 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,7 +13,5 @@ before_script:
 
 script:
   - if [[ $(rustup show active-toolchain) == stable* ]]; then cargo fmt -- --check; fi;
-  - cargo build
-  - cargo build --no-default-features
-  - cargo test
+  - cargo test --all-features
   - cargo test --no-default-features
diff --git a/Cargo.toml b/Cargo.toml
index 901846e..40bf99c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "quick-xml"
-version = "0.17.0"
+version = "0.17.1"
 authors = ["Johann Tuffe <tafia973@gmail.com>"]
 description = "High performance xml reader and writer"
 
@@ -17,11 +17,19 @@ travis-ci = { repository = "tafia/quick-xml" }
 
 [dependencies]
 encoding_rs = { version = "0.8.17", optional = true }
+serde = { version = "1.0", optional = true }
 memchr = "2.2.1"
 
+[dev-dependencies]
+serde = { version = "1.0", features = ["derive"] }
+
 [lib]
 bench = false
 
 [features]
 default = []
 encoding = ["encoding_rs"]
+serialize = ["serde"]
+
+[package.metadata.docs.rs]
+features = ["serialize"]
diff --git a/Changelog.md b/Changelog.md
index 41b30fa..8004ca8 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -8,6 +8,9 @@
   - test: Adding missing tests
   - chore: Changes to the build process or auxiliary tools/libraries/documentation
 
+## 0.17.1
+- feat: add new `serialize` feature to support serde serialize/deserialize
+
 ## 0.17.0
 - perf: speed up (un)escape a little
 - feat: remove failure completely (breaking change) and implement `std::error::Error` for `Error`
diff --git a/README.md b/README.md
index eb99d62..4efac59 100644
--- a/README.md
+++ b/README.md
@@ -108,6 +108,7 @@ assert_eq!(result, expected.as_bytes());
 # Features
 
 - `encoding`: support non utf8 xmls
+- `serialize`: support serde `Serialize`/`Deserialize`
 
 ## Performance
 
diff --git a/src/de/errors.rs b/src/de/errors.rs
new file mode 100644
index 0000000..7d16b14
--- /dev/null
+++ b/src/de/errors.rs
@@ -0,0 +1,89 @@
+use crate::errors::Error;
+use std::fmt;
+
+/// Deserialization error
+#[derive(Debug)]
+pub enum DeError {
+    /// Serde custom error
+    Custom(String),
+    /// Cannot parse to integer
+    Int(std::num::ParseIntError),
+    /// Cannot parse to float
+    Float(std::num::ParseFloatError),
+    /// Xml parsing error
+    Xml(Error),
+    /// Unexpected end of attributes
+    EndOfAttributes,
+    /// Unexpected end of file
+    Eof,
+    /// Invalid value for a boolean
+    InvalidBoolean(String),
+    /// Invalid unit value
+    InvalidUnit(String),
+    /// Invalid event for Enum
+    InvalidEnum(crate::events::Event<'static>),
+    /// Expecting Text event
+    Text,
+    /// Expecting Start event
+    Start,
+    /// Expecting End event
+    End,
+}
+
+impl fmt::Display for DeError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+        match self {
+            DeError::Custom(s) => write!(f, "{}", s),
+            DeError::Xml(e) => write!(f, "{}", e),
+            DeError::Int(e) => write!(f, "{}", e),
+            DeError::Float(e) => write!(f, "{}", e),
+            DeError::EndOfAttributes => write!(f, "Unexpected end of attributes"),
+            DeError::Eof => write!(f, "Unexpected `Event::Eof`"),
+            DeError::InvalidBoolean(v) => write!(f, "Invalid boolean value '{}'", v),
+            DeError::InvalidUnit(v) => {
+                write!(f, "Invalid unit value '{}', expected empty string", v)
+            }
+            DeError::InvalidEnum(e) => write!(
+                f,
+                "Invalid event for Enum, expecting Text or Start, got: {:?}",
+                e
+            ),
+            DeError::Text => write!(f, "Expecting Text event"),
+            DeError::Start => write!(f, "Expecting Start event"),
+            DeError::End => write!(f, "Expecting End event"),
+        }
+    }
+}
+
+impl ::std::error::Error for DeError {
+    fn description(&self) -> &str {
+        "xml deserialize error"
+    }
+    fn cause(&self) -> Option<&dyn (::std::error::Error)> {
+        None
+    }
+}
+
+impl serde::de::Error for DeError {
+    fn custom<T: fmt::Display>(msg: T) -> Self {
+        DeError::Custom(msg.to_string())
+    }
+}
+
+impl From<Error> for DeError {
+    fn from(e: Error) -> Self {
+        DeError::Xml(e)
+    }
+}
+
+impl From<std::num::ParseIntError> for DeError {
+    fn from(e: std::num::ParseIntError) -> Self {
+        DeError::Int(e)
+    }
+}
+
+impl From<std::num::ParseFloatError> for DeError {
+    fn from(e: std::num::ParseFloatError) -> Self {
+        DeError::Float(e)
+    }
+}
diff --git a/src/de/escape.rs b/src/de/escape.rs
new file mode 100644
index 0000000..5b5d8cf
--- /dev/null
+++ b/src/de/escape.rs
@@ -0,0 +1,244 @@
+//! Serde `Deserializer` module
+
+use crate::{de::errors::DeError, errors::Error, escape::unescape, reader::Decoder};
+use serde::de::{self, Visitor};
+use serde::{self, forward_to_deserialize_any};
+use std::borrow::Cow;
+
+/// A deserializer for a xml escaped and encoded value
+///
+/// # Note
+///
+/// Escaping the value is actually not always necessary, for instance
+/// when converting to float, we don't expect any escapable character
+/// anyway
+#[derive(Clone)]
+pub(crate) struct EscapedDeserializer {
+    decoder: Decoder,
+    escaped_value: Vec<u8>,
+    escaped: bool,
+}
+
+impl EscapedDeserializer {
+    pub fn new(escaped_value: Vec<u8>, decoder: Decoder, escaped: bool) -> Self {
+        EscapedDeserializer {
+            decoder,
+            escaped_value,
+            escaped,
+        }
+    }
+    fn unescaped(&self) -> Result<Cow<[u8]>, DeError> {
+        if self.escaped {
+            unescape(&self.escaped_value).map_err(|e| DeError::Xml(Error::EscapeError(e)))
+        } else {
+            Ok(Cow::Borrowed(&self.escaped_value))
+        }
+    }
+}
+
+macro_rules! deserialize_num {
+    ($method:ident, $visit:ident) => {
+        fn $method<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+        where
+            V: Visitor<'de>,
+        {
+            #[cfg(not(feature = "encoding"))]
+            let value = self.decoder.decode(&self.escaped_value)?.parse()?;
+
+            #[cfg(feature = "encoding")]
+            let value = self.decoder.decode(&self.escaped_value).parse()?;
+
+            visitor.$visit(value)
+        }
+    }
+}
+
+impl<'de> serde::Deserializer<'de> for EscapedDeserializer {
+    type Error = DeError;
+
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        self.deserialize_str(visitor)
+    }
+
+    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        let unescaped = self.unescaped()?;
+        #[cfg(not(feature = "encoding"))]
+        let value = self.decoder.decode(&unescaped)?;
+
+        #[cfg(feature = "encoding")]
+        let value = self.decoder.decode(&unescaped);
+        visitor.visit_str(&value)
+    }
+
+    fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        let v = self.unescaped()?;
+        visitor.visit_bytes(&v)
+    }
+
+    fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        self.deserialize_bytes(visitor)
+    }
+
+    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        self.deserialize_str(visitor)
+    }
+
+    fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        #[cfg(feature = "encoding")]
+        {
+            #[cfg(feature = "encoding")]
+            let value = self.decoder.decode(&self.escaped_value);
+
+            match value.as_ref() {
+                "true" | "1" | "True" | "TRUE" | "t" | "Yes" | "YES" | "yes" | "y" => {
+                    visitor.visit_bool(true)
+                }
+                "false" | "0" | "False" | "FALSE" | "f" | "No" | "NO" | "no" | "n" => {
+                    visitor.visit_bool(false)
+                }
+                _ => Err(DeError::InvalidBoolean(value.into())),
+            }
+        }
+
+        #[cfg(not(feature = "encoding"))]
+        {
+            match &*self.escaped_value {
+                b"true" | b"1" | b"True" | b"TRUE" | b"t" | b"Yes" | b"YES" | b"yes" | b"y" => {
+                    visitor.visit_bool(true)
+                }
+                b"false" | b"0" | b"False" | b"FALSE" | b"f" | b"No" | b"NO" | b"no" | b"n" => {
+                    visitor.visit_bool(false)
+                }
+                e => Err(DeError::InvalidBoolean(self.decoder.decode(e)?.into())),
+            }
+        }
+    }
+
+    fn deserialize_char<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        self.deserialize_str(visitor)
+    }
+
+    fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        if self.escaped_value.is_empty() {
+            visitor.visit_unit()
+        } else {
+            Err(DeError::InvalidUnit(
+                "Expecting unit, got non empty attribute".into(),
+            ))
+        }
+    }
+
+    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        if self.escaped_value.is_empty() {
+            visitor.visit_none()
+        } else {
+            visitor.visit_some(self)
+        }
+    }
+
+    fn deserialize_enum<V: de::Visitor<'de>>(
+        self,
+        _name: &str,
+        _variants: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Self::Error> {
+        visitor.visit_enum(self)
+    }
+
+    fn deserialize_newtype_struct<V>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        visitor.visit_newtype_struct(self)
+    }
+
+    deserialize_num!(deserialize_i64, visit_i64);
+    deserialize_num!(deserialize_i32, visit_i32);
+    deserialize_num!(deserialize_i16, visit_i16);
+    deserialize_num!(deserialize_i8, visit_i8);
+    deserialize_num!(deserialize_u64, visit_u64);
+    deserialize_num!(deserialize_u32, visit_u32);
+    deserialize_num!(deserialize_u16, visit_u16);
+    deserialize_num!(deserialize_u8, visit_u8);
+    deserialize_num!(deserialize_f64, visit_f64);
+    deserialize_num!(deserialize_f32, visit_f32);
+
+    forward_to_deserialize_any! {
+        unit_struct seq tuple tuple_struct map struct identifier ignored_any
+    }
+}
+
+impl<'de> de::EnumAccess<'de> for EscapedDeserializer {
+    type Error = DeError;
+    type Variant = Self;
+
+    fn variant_seed<V: de::DeserializeSeed<'de>>(
+        self,
+        seed: V,
+    ) -> Result<(V::Value, Self), DeError> {
+        let name = seed.deserialize(self.clone())?;
+        Ok((name, self))
+    }
+}
+
+impl<'de> de::VariantAccess<'de> for EscapedDeserializer {
+    type Error = DeError;
+
+    fn unit_variant(self) -> Result<(), DeError> {
+        Ok(())
+    }
+
+    fn newtype_variant_seed<T: de::DeserializeSeed<'de>>(
+        self,
+        seed: T,
+    ) -> Result<T::Value, DeError> {
+        seed.deserialize(self)
+    }
+
+    fn tuple_variant<V: de::Visitor<'de>>(
+        self,
+        _len: usize,
+        _visitor: V,
+    ) -> Result<V::Value, DeError> {
+        unimplemented!()
+    }
+
+    fn struct_variant<V: de::Visitor<'de>>(
+        self,
+        _fields: &'static [&'static str],
+        _visitor: V,
+    ) -> Result<V::Value, DeError> {
+        unimplemented!()
+    }
+}
diff --git a/src/de/map.rs b/src/de/map.rs
new file mode 100644
index 0000000..193c376
--- /dev/null
+++ b/src/de/map.rs
@@ -0,0 +1,95 @@
+//! Serde `Deserializer` module
+
+use crate::{
+    de::{errors::DeError, escape::EscapedDeserializer, Deserializer, INNER_VALUE},
+    events::{attributes::Attribute, BytesStart, Event},
+};
+use serde::de::{self, DeserializeSeed, IntoDeserializer};
+use std::io::BufRead;
+
+enum MapValue {
+    Empty,
+    Attribute { value: Vec<u8> },
+    Nested,
+    InnerValue,
+}
+
+/// A deserializer for `Attributes`
+pub(crate) struct MapAccess<'a, R: BufRead> {
+    start: BytesStart<'static>,
+    de: &'a mut Deserializer<R>,
+    position: usize,
+    value: MapValue,
+}
+
+impl<'a, R: BufRead> MapAccess<'a, R> {
+    /// Create a new MapAccess
+    pub fn new(de: &'a mut Deserializer<R>, start: BytesStart<'static>) -> Result<Self, DeError> {
+        let position = start.attributes().position;
+        Ok(MapAccess {
+            de,
+            start,
+            position,
+            value: MapValue::Empty,
+        })
+    }
+
+    fn next_attr(&mut self) -> Result<Option<Attribute>, DeError> {
+        let mut attributes = self.start.attributes();
+        attributes.position = self.position;
+        let next_att = attributes.next();
+        self.position = attributes.position;
+        Ok(next_att.transpose()?)
+    }
+}
+
+impl<'a, 'de, R: BufRead> de::MapAccess<'de> for MapAccess<'a, R> {
+    type Error = DeError;
+
+    fn next_key_seed<K: DeserializeSeed<'de>>(
+        &mut self,
+        seed: K,
+    ) -> Result<Option<K::Value>, Self::Error> {
+        let attr_key_val = self
+            .next_attr()?
+            .map(|a| (a.key.to_owned(), a.value.into_owned()));
+        let decoder = self.de.reader.decoder();
+        let has_value_field = self.de.has_value_field;
+        if let Some((key, value)) = attr_key_val {
+            // try getting map from attributes (key= "value")
+            self.value = MapValue::Attribute { value };
+            seed.deserialize(EscapedDeserializer::new(key, decoder, false))
+                .map(Some)
+        } else {
+            // try getting from events (<key>value</key>)
+            match self.de.peek()? {
+                Some(Event::Text(_)) | Some(Event::Start(_)) if has_value_field => {
+                    self.value = MapValue::InnerValue;
+                    seed.deserialize(INNER_VALUE.into_deserializer()).map(Some)
+                }
+                Some(Event::Start(e)) => {
+                    let name = e.name().to_owned();
+                    self.value = MapValue::Nested;
+                    seed.deserialize(EscapedDeserializer::new(name, decoder, false))
+                        .map(Some)
+                }
+                _ => Ok(None),
+            }
+        }
+    }
+
+    fn next_value_seed<K: DeserializeSeed<'de>>(
+        &mut self,
+        seed: K,
+    ) -> Result<K::Value, Self::Error> {
+        match std::mem::replace(&mut self.value, MapValue::Empty) {
+            MapValue::Attribute { value } => seed.deserialize(EscapedDeserializer::new(
+                value,
+                self.de.reader.decoder(),
+                true,
+            )),
+            MapValue::Nested | MapValue::InnerValue => seed.deserialize(&mut *self.de),
+            MapValue::Empty => Err(DeError::EndOfAttributes),
+        }
+    }
+}
diff --git a/src/de/mod.rs b/src/de/mod.rs
new file mode 100644
index 0000000..ecf5863
--- /dev/null
+++ b/src/de/mod.rs
@@ -0,0 +1,563 @@
+//! Serde `Deserializer` module
+//!
+//! # Examples
+//!
+//! Here is a simple example parsing [crates.io](https://crates.io/) source code.
+//!
+//! ```
+//! // Cargo.toml
+//! // [dependencies]
+//! // serde = { version = "1.0", features = [ "derive" ] }
+//! // quick_xml = "0.17"
+//! extern crate serde;
+//! extern crate quick_xml;
+//!
+//! use serde::Deserialize;
+//! use quick_xml::de::{from_str, DeError};
+//!
+//! #[derive(Debug, Deserialize, PartialEq)]
+//! struct Link {
+//!     rel: String,
+//!     href: String,
+//!     sizes: Option<String>,
+//! }
+//!
+//! #[derive(Debug, Deserialize, PartialEq)]
+//! #[serde(rename_all = "lowercase")]
+//! enum Lang {
+//!     En,
+//!     Fr,
+//!     De,
+//! }
+//!
+//! #[derive(Debug, Deserialize, PartialEq)]
+//! struct Head {
+//!     title: String,
+//!     #[serde(rename = "link", default)]
+//!     links: Vec<Link>,
+//! }
+//!
+//! #[derive(Debug, Deserialize, PartialEq)]
+//! struct Script {
+//!     src: String,
+//!     integrity: String,
+//! }
+//!
+//! #[derive(Debug, Deserialize, PartialEq)]
+//! struct Body {
+//!     #[serde(rename = "script", default)]
+//!     scripts: Vec<Script>,
+//! }
+//!
+//! #[derive(Debug, Deserialize, PartialEq)]
+//! struct Html {
+//!     lang: Option<String>,
+//!     head: Head,
+//!     body: Body,
+//! }
+//!
+//! fn crates_io() -> Result<Html, DeError> {
+//!     let xml = "<!DOCTYPE html>
+//!         <html lang=\"en\">
+//!           <head>
+//!             <meta charset=\"utf-8\">
+//!             <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">
+//!             <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
+//!
+//!             <title>crates.io: Rust Package Registry</title>
+//!
+//!
+//!         <meta name=\"cargo/config/environment\" content=\"%7B%22modulePrefix%22%3A%22cargo%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22router-scroll%22%2C%22historySupportMiddleware%22%3Atrue%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%7D%7D%2C%22APP%22%3A%7B%22name%22%3A%22cargo%22%2C%22version%22%3A%22b7796c9%22%7D%2C%22fastboot%22%3A%7B%22hostWhitelist%22%3A%5B%22crates.io%22%2C%7B%7D%2C%7B%7D%5D%7D%2C%22ember-cli-app-version%22%3A%7B%22version%22%3A%22b7796c9%22%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%7D\" />
+//!         <!-- EMBER_CLI_FASTBOOT_TITLE --><!-- EMBER_CLI_FASTBOOT_HEAD -->
+//!         <link rel=\"manifest\" href=\"/manifest.webmanifest\">
+//!         <link rel=\"apple-touch-icon\" href=\"/cargo-835dd6a18132048a52ac569f2615b59d.png\" sizes=\"227x227\">
+//!         <meta name=\"theme-color\" content=\"#f9f7ec\">
+//!         <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">
+//!         <meta name=\"apple-mobile-web-app-title\" content=\"crates.io: Rust Package Registry\">
+//!         <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"default\">
+//!
+//!             <link rel=\"stylesheet\" href=\"/assets/vendor-8d023d47762d5431764f589a6012123e.css\" integrity=\"sha256-EoB7fsYkdS7BZba47+C/9D7yxwPZojsE4pO7RIuUXdE= sha512-/SzGQGR0yj5AG6YPehZB3b6MjpnuNCTOGREQTStETobVRrpYPZKneJwcL/14B8ufcvobJGFDvnTKdcDDxbh6/A==\" >
+//!             <link rel=\"stylesheet\" href=\"/assets/cargo-cedb8082b232ce89dd449d869fb54b98.css\" integrity=\"sha256-S9K9jZr6nSyYicYad3JdiTKrvsstXZrvYqmLUX9i3tc= sha512-CDGjy3xeyiqBgUMa+GelihW394pqAARXwsU+HIiOotlnp1sLBVgO6v2ZszL0arwKU8CpvL9wHyLYBIdfX92YbQ==\" >
+//!
+//!
+//!             <link rel=\"shortcut icon\" href=\"/favicon.ico\" type=\"image/x-icon\">
+//!             <link rel=\"icon\" href=\"/cargo-835dd6a18132048a52ac569f2615b59d.png\" type=\"image/png\">
+//!             <link rel=\"search\" href=\"/opensearch.xml\" type=\"application/opensearchdescription+xml\" title=\"Cargo\">
+//!           </head>
+//!           <body>
+//!             <!-- EMBER_CLI_FASTBOOT_BODY -->
+//!             <noscript>
+//!                 <div id=\"main\">
+//!                     <div class='noscript'>
+//!                         This site requires JavaScript to be enabled.
+//!                     </div>
+//!                 </div>
+//!             </noscript>
+//!
+//!             <script src=\"/assets/vendor-bfe89101b20262535de5a5ccdc276965.js\" integrity=\"sha256-U12Xuwhz1bhJXWyFW/hRr+Wa8B6FFDheTowik5VLkbw= sha512-J/cUUuUN55TrdG8P6Zk3/slI0nTgzYb8pOQlrXfaLgzr9aEumr9D1EzmFyLy1nrhaDGpRN1T8EQrU21Jl81pJQ==\" ></script>
+//!             <script src=\"/assets/cargo-4023b68501b7b3e17b2bb31f50f5eeea.js\" integrity=\"sha256-9atimKc1KC6HMJF/B07lP3Cjtgr2tmET8Vau0Re5mVI= sha512-XJyBDQU4wtA1aPyPXaFzTE5Wh/mYJwkKHqZ/Fn4p/ezgdKzSCFu6FYn81raBCnCBNsihfhrkb88uF6H5VraHMA==\" ></script>
+//!
+//!
+//!           </body>
+//!         </html>
+//! }";
+//!     let html: Html = from_str(xml)?;
+//!     assert_eq!(&html.head.title, "crates.io: Rust Package Registr");
+//!     Ok(html)
+//! }
+//! ```
+
+mod errors;
+mod escape;
+mod map;
+mod seq;
+mod var;
+
+pub use self::errors::DeError;
+use crate::{
+    events::{BytesStart, BytesText, Event},
+    Reader,
+};
+use serde::de::{self, DeserializeOwned};
+use serde::forward_to_deserialize_any;
+use std::io::BufRead;
+
+const INNER_VALUE: &str = "$value";
+
+/// An xml deserializer
+pub struct Deserializer<R: BufRead> {
+    reader: Reader<R>,
+    peek: Option<Event<'static>>,
+    has_value_field: bool,
+}
+
+/// Deserialize a xml string
+pub fn from_str<T: DeserializeOwned>(s: &str) -> Result<T, DeError> {
+    from_reader(s.as_bytes())
+}
+
+/// Deserialize from a reader
+pub fn from_reader<R: BufRead, T: DeserializeOwned>(reader: R) -> Result<T, DeError> {
+    let mut de = Deserializer::from_reader(reader);
+    T::deserialize(&mut de)
+}
+
+impl<R: BufRead> Deserializer<R> {
+    /// Get a new deserializer
+    pub fn new(reader: Reader<R>) -> Self {
+        Deserializer {
+            reader,
+            peek: None,
+            has_value_field: false,
+        }
+    }
+
+    /// Get a new deserializer from a regular BufRead
+    pub fn from_reader(reader: R) -> Self {
+        let mut reader = Reader::from_reader(reader);
+        reader
+            .expand_empty_elements(true)
+            .check_end_names(true)
+            .trim_text(true);
+        Self::new(reader)
+    }
+
+    fn peek(&mut self) -> Result<Option<&Event<'static>>, DeError> {
+        if self.peek.is_none() {
+            self.peek = Some(self.next(&mut Vec::new())?);
+        }
+        Ok(self.peek.as_ref())
+    }
+
+    fn next<'a>(&mut self, buf: &'a mut Vec<u8>) -> Result<Event<'static>, DeError> {
+        if let Some(e) = self.peek.take() {
+            return Ok(e);
+        }
+        loop {
+            let e = self.reader.read_event(buf)?;
+            match e {
+                Event::Start(_) | Event::End(_) | Event::Text(_) | Event::Eof => {
+                    return Ok(e.into_owned())
+                }
+                _ => buf.clear(),
+            }
+        }
+    }
+
+    fn next_start(&mut self, buf: &mut Vec<u8>) -> Result<Option<BytesStart<'static>>, DeError> {
+        loop {
+            let e = self.next(buf)?;
+            match e {
+                Event::Start(e) => return Ok(Some(e)),
+                Event::End(_) => return Err(DeError::End),
+                Event::Eof => return Ok(None),
+                _ => buf.clear(), // ignore texts
+            }
+        }
+    }
+
+    fn next_text<'a>(&mut self) -> Result<BytesText<'static>, DeError> {
+        match self.next(&mut Vec::new())? {
+            Event::Text(e) => Ok(e),
+            Event::Eof => Err(DeError::Eof),
+            Event::Start(e) => {
+                // allow one nested level
+                let t = match self.next(&mut Vec::new())? {
+                    Event::Text(t) => t,
+                    Event::Start(_) => return Err(DeError::Start),
+                    Event::End(_) => return Err(DeError::End),
+                    Event::Eof => return Err(DeError::Eof),
+                    _ => unreachable!(),
+                };
+                self.read_to_end(e.name())?;
+                Ok(t)
+            }
+            Event::End(_) => Err(DeError::End),
+            _ => unreachable!(),
+        }
+    }
+
+    fn read_to_end(&mut self, name: &[u8]) -> Result<(), DeError> {
+        let mut buf = Vec::new();
+        match self.next(&mut buf)? {
+            Event::Start(e) => self.reader.read_to_end(e.name(), &mut Vec::new())?,
+            Event::End(e) if e.name() == name => return Ok(()),
+            _ => buf.clear(),
+        }
+        Ok(self.reader.read_to_end(name, &mut buf)?)
+    }
+}
+
+macro_rules! deserialize_type {
+    ($deserialize:ident => $visit:ident) => {
+        fn $deserialize<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
+            let txt = self.next_text()?;
+
+            #[cfg(not(feature = "encoding"))]
+            let value = self.reader.decode(&*txt)?.parse()?;
+
+            #[cfg(feature = "encoding")]
+            let value = self.reader.decode(&*txt).parse()?;
+
+            visitor.$visit(value)
+        }
+    }
+}
+
+impl<'de, 'a, R: BufRead> de::Deserializer<'de> for &'a mut Deserializer<R> {
+    type Error = DeError;
+
+    forward_to_deserialize_any! { newtype_struct identifier }
+
+    fn deserialize_struct<V: de::Visitor<'de>>(
+        self,
+        _name: &'static str,
+        fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, DeError> {
+        if let Some(e) = self.next_start(&mut Vec::new())? {
+            let name = e.name().to_vec();
+            self.has_value_field = fields.contains(&INNER_VALUE);
+            let map = map::MapAccess::new(self, e)?;
+            let value = visitor.visit_map(map)?;
+            self.has_value_field = false;
+            self.read_to_end(&name)?;
+            Ok(value)
+        } else {
+            Err(DeError::Start)
+        }
+    }
+
+    deserialize_type!(deserialize_i8 => visit_i8);
+    deserialize_type!(deserialize_i16 => visit_i16);
+    deserialize_type!(deserialize_i32 => visit_i32);
+    deserialize_type!(deserialize_i64 => visit_i64);
+    deserialize_type!(deserialize_u8 => visit_u8);
+    deserialize_type!(deserialize_u16 => visit_u16);
+    deserialize_type!(deserialize_u32 => visit_u32);
+    deserialize_type!(deserialize_u64 => visit_u64);
+    deserialize_type!(deserialize_f32 => visit_f32);
+    deserialize_type!(deserialize_f64 => visit_f64);
+
+    fn deserialize_bool<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
+        let txt = self.next_text()?;
+
+        #[cfg(feature = "encoding")]
+        {
+            #[cfg(feature = "encoding")]
+            let value = self.reader.decode(&*txt);
+
+            match value.as_ref() {
+                "true" | "1" | "True" | "TRUE" | "t" | "Yes" | "YES" | "yes" | "y" => {
+                    visitor.visit_bool(true)
+                }
+                "false" | "0" | "False" | "FALSE" | "f" | "No" | "NO" | "no" | "n" => {
+                    visitor.visit_bool(false)
+                }
+                _ => Err(DeError::InvalidBoolean(value.into())),
+            }
+        }
+
+        #[cfg(not(feature = "encoding"))]
+        {
+            match txt.as_ref() {
+                b"true" | b"1" | b"True" | b"TRUE" | b"t" | b"Yes" | b"YES" | b"yes" | b"y" => {
+                    visitor.visit_bool(true)
+                }
+                b"false" | b"0" | b"False" | b"FALSE" | b"f" | b"No" | b"NO" | b"no" | b"n" => {
+                    visitor.visit_bool(false)
+                }
+                e => Err(DeError::InvalidBoolean(self.reader.decode(e)?.into())),
+            }
+        }
+    }
+
+    fn deserialize_string<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
+        let value = self.next_text()?.unescape_and_decode(&self.reader)?;
+        visitor.visit_string(value)
+    }
+
+    fn deserialize_char<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
+        self.deserialize_string(visitor)
+    }
+
+    fn deserialize_str<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
+        self.deserialize_string(visitor)
+    }
+
+    fn deserialize_bytes<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
+        self.deserialize_string(visitor)
+    }
+
+    fn deserialize_byte_buf<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
+        self.deserialize_string(visitor)
+    }
+
+    fn deserialize_unit<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
+        let value = self.next_text()?;
+        if value.is_empty() {
+            visitor.visit_unit()
+        } else {
+            Err(DeError::InvalidUnit(
+                value.unescape_and_decode(&self.reader)?,
+            ))
+        }
+    }
+
+    fn deserialize_unit_struct<V: de::Visitor<'de>>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value, DeError> {
+        self.deserialize_unit(visitor)
+    }
+
+    fn deserialize_tuple<V: de::Visitor<'de>>(
+        self,
+        len: usize,
+        visitor: V,
+    ) -> Result<V::Value, DeError> {
+        visitor.visit_seq(seq::SeqAccess::new(self, Some(len))?)
+    }
+
+    fn deserialize_tuple_struct<V: de::Visitor<'de>>(
+        self,
+        _name: &'static str,
+        len: usize,
+        visitor: V,
+    ) -> Result<V::Value, DeError> {
+        self.deserialize_tuple(len, visitor)
+    }
+
+    fn deserialize_enum<V: de::Visitor<'de>>(
+        self,
+        _name: &'static str,
+        _variants: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, DeError> {
+        let value = visitor.visit_enum(var::EnumAccess::new(self))?;
+        Ok(value)
+    }
+
+    fn deserialize_seq<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
+        visitor.visit_seq(seq::SeqAccess::new(self, None)?)
+    }
+
+    fn deserialize_map<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
+        self.deserialize_struct("", &[], visitor)
+    }
+
+    fn deserialize_option<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
+        match self.peek()? {
+            Some(Event::Text(t)) if t.is_empty() => visitor.visit_none(),
+            None | Some(Event::Eof) => visitor.visit_none(),
+            _ => visitor.visit_some(self),
+        }
+    }
+
+    fn deserialize_ignored_any<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
+        match self.next(&mut Vec::new())? {
+            Event::Start(e) => self.read_to_end(e.name())?,
+            Event::End(_) => return Err(DeError::End),
+            _ => (),
+        }
+        visitor.visit_unit()
+    }
+
+    fn deserialize_any<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
+        match self.peek()?.ok_or(DeError::Eof)? {
+            Event::Start(_) => self.deserialize_map(visitor),
+            Event::End(_) => self.deserialize_unit(visitor),
+            _ => self.deserialize_string(visitor),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use serde::Deserialize;
+
+    #[derive(Debug, Deserialize, PartialEq)]
+    struct Item {
+        name: String,
+        source: String,
+    }
+
+    #[test]
+    fn simple_struct_from_attributes() {
+        let s = r##"
+	    <item name="hello" source="world.rs" />
+	"##;
+
+        let item: Item = from_str(s).unwrap();
+
+        assert_eq!(
+            item,
+            Item {
+                name: "hello".to_string(),
+                source: "world.rs".to_string(),
+            }
+        );
+    }
+
+    #[test]
+    fn multiple_roots_attributes() {
+        let s = r##"
+	    <item name="hello" source="world.rs" />
+	    <item name="hello" source="world.rs" />
+	"##;
+
+        let item: Vec<Item> = from_str(s).unwrap();
+
+        assert_eq!(
+            item,
+            vec![
+                Item {
+                    name: "hello".to_string(),
+                    source: "world.rs".to_string(),
+                },
+                Item {
+                    name: "hello".to_string(),
+                    source: "world.rs".to_string(),
+                },
+            ]
+        );
+    }
+
+    #[test]
+    fn simple_struct_from_attribute_and_child() {
+        let s = r##"
+	    <item name="hello">
+            <source>world.rs</source>
+            </item>
+        "##;
+
+        let item: Item = from_str(s).unwrap();
+
+        assert_eq!(
+            item,
+            Item {
+                name: "hello".to_string(),
+                source: "world.rs".to_string(),
+            }
+        );
+    }
+
+    #[derive(Debug, Deserialize, PartialEq)]
+    struct Project {
+        name: String,
+
+        #[serde(rename = "item", default)]
+        items: Vec<Item>,
+    }
+
+    #[test]
+    fn nested_collection() {
+        let s = r##"
+	    <project name="my_project">
+		<item name="hello1" source="world1.rs" />
+		<item name="hello2" source="world2.rs" />
+	    </project>
+	"##;
+
+        let project: Project = from_str(s).unwrap();
+
+        assert_eq!(
+            project,
+            Project {
+                name: "my_project".to_string(),
+                items: vec![
+                    Item {
+                        name: "hello1".to_string(),
+                        source: "world1.rs".to_string(),
+                    },
+                    Item {
+                        name: "hello2".to_string(),
+                        source: "world2.rs".to_string(),
+                    },
+                ],
+            }
+        );
+    }
+
+    #[derive(Debug, Deserialize, PartialEq)]
+    enum MyEnum {
+        A(String),
+        B { name: String, flag: bool },
+        C,
+    }
+
+    #[derive(Debug, Deserialize, PartialEq)]
+    struct MyEnums {
+        #[serde(rename = "$value")]
+        items: Vec<MyEnum>,
+    }
+
+    #[test]
+    fn collection_of_enums() {
+        let s = r##"
+	    <enums>
+		<A>test</A>
+		<B name="hello" flag="t" />
+		<C />
+	    </enums>
+	"##;
+
+        let project: MyEnums = from_str(s).unwrap();
+
+        assert_eq!(
+            project,
+            MyEnums {
+                items: vec![
+                    MyEnum::A("test".to_string()),
+                    MyEnum::B {
+                        name: "hello".to_string(),
+                        flag: true,
+                    },
+                    MyEnum::C,
+                ],
+            }
+        );
+    }
+}
diff --git a/src/de/seq.rs b/src/de/seq.rs
new file mode 100644
index 0000000..eeb8224
--- /dev/null
+++ b/src/de/seq.rs
@@ -0,0 +1,85 @@
+use crate::de::{DeError, Deserializer};
+use crate::{
+    events::{BytesStart, Event},
+    reader::Decoder,
+};
+use serde::de;
+use std::io::BufRead;
+
+#[derive(Debug)]
+enum Names {
+    Unknown,
+    Peek(String),
+}
+
+impl Names {
+    fn is_valid(&self, decoder: Decoder, start: &BytesStart) -> Result<bool, DeError> {
+        #[cfg(not(feature = "encoding"))]
+        let name = decoder.decode(start.name())?;
+        #[cfg(feature = "encoding")]
+        let name = decoder.decode(start.name());
+        let res = match self {
+            Names::Unknown => true,
+            Names::Peek(n) => &**n == &*name,
+        };
+        Ok(res)
+    }
+}
+
+/// A SeqAccess
+pub struct SeqAccess<'a, R: BufRead> {
+    de: &'a mut Deserializer<R>,
+    max_size: Option<usize>,
+    names: Names,
+}
+
+impl<'a, R: BufRead> SeqAccess<'a, R> {
+    /// Get a new SeqAccess
+    pub fn new(de: &'a mut Deserializer<R>, max_size: Option<usize>) -> Result<Self, DeError> {
+        let decoder = de.reader.decoder();
+        let names = if de.has_value_field {
+            Names::Unknown
+        } else {
+            if let Some(Event::Start(e)) = de.peek()? {
+                #[cfg(not(feature = "encoding"))]
+                let name = decoder.decode(e.name())?.to_owned();
+                #[cfg(feature = "encoding")]
+                let name = decoder.decode(e.name()).into_owned();
+                Names::Peek(name)
+            } else {
+                Names::Unknown
+            }
+        };
+        Ok(SeqAccess {
+            de,
+            max_size,
+            names,
+        })
+    }
+}
+
+impl<'de, 'a, R: 'a + BufRead> de::SeqAccess<'de> for SeqAccess<'a, R> {
+    type Error = DeError;
+
+    fn size_hint(&self) -> Option<usize> {
+        self.max_size
+    }
+
+    fn next_element_seed<T: de::DeserializeSeed<'de>>(
+        &mut self,
+        seed: T,
+    ) -> Result<Option<T::Value>, DeError> {
+        if let Some(s) = self.max_size.as_mut() {
+            if *s == 0 {
+                return Ok(None);
+            }
+            *s -= 1;
+        }
+        let decoder = self.de.reader.decoder();
+        match self.de.peek()? {
+            None | Some(Event::Eof) | Some(Event::End(_)) => Ok(None),
+            Some(Event::Start(e)) if !self.names.is_valid(decoder, e)? => Ok(None),
+            _ => seed.deserialize(&mut *self.de).map(Some),
+        }
+    }
+}
diff --git a/src/de/var.rs b/src/de/var.rs
new file mode 100644
index 0000000..6ec0564
--- /dev/null
+++ b/src/de/var.rs
@@ -0,0 +1,74 @@
+use crate::de::{errors::DeError, escape::EscapedDeserializer, Deserializer};
+use crate::events::Event;
+use serde::de::{self, Deserializer as SerdeDeserializer};
+use std::io::BufRead;
+
+/// An enum access
+pub struct EnumAccess<'a, R: BufRead> {
+    de: &'a mut Deserializer<R>,
+}
+
+impl<'a, R: BufRead> EnumAccess<'a, R> {
+    pub fn new(de: &'a mut Deserializer<R>) -> Self {
+        EnumAccess { de }
+    }
+}
+
+impl<'de, 'a, R: 'a + BufRead> de::EnumAccess<'de> for EnumAccess<'a, R> {
+    type Error = DeError;
+    type Variant = VariantAccess<'a, R>;
+
+    fn variant_seed<V: de::DeserializeSeed<'de>>(
+        self,
+        seed: V,
+    ) -> Result<(V::Value, VariantAccess<'a, R>), DeError> {
+        let decoder = self.de.reader.decoder();
+        let de = match self.de.peek()? {
+            Some(Event::Text(t)) => EscapedDeserializer::new(t.to_vec(), decoder, true),
+            Some(Event::Start(e)) => EscapedDeserializer::new(e.name().to_vec(), decoder, false),
+            Some(e) => return Err(DeError::InvalidEnum(e.to_owned())),
+            None => return Err(DeError::Eof),
+        };
+        let name = seed.deserialize(de)?;
+        Ok((name, VariantAccess { de: self.de }))
+    }
+}
+
+pub struct VariantAccess<'a, R: BufRead> {
+    de: &'a mut Deserializer<R>,
+}
+
+impl<'de, 'a, R: BufRead> de::VariantAccess<'de> for VariantAccess<'a, R> {
+    type Error = DeError;
+
+    fn unit_variant(self) -> Result<(), DeError> {
+        match self.de.next(&mut Vec::new())? {
+            Event::Start(e) => self.de.read_to_end(e.name()),
+            Event::Text(_) => Ok(()),
+            _ => unreachable!(),
+        }
+    }
+
+    fn newtype_variant_seed<T: de::DeserializeSeed<'de>>(
+        self,
+        seed: T,
+    ) -> Result<T::Value, DeError> {
+        seed.deserialize(&mut *self.de)
+    }
+
+    fn tuple_variant<V: de::Visitor<'de>>(
+        self,
+        len: usize,
+        visitor: V,
+    ) -> Result<V::Value, DeError> {
+        self.de.deserialize_tuple(len, visitor)
+    }
+
+    fn struct_variant<V: de::Visitor<'de>>(
+        self,
+        fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, DeError> {
+        self.de.deserialize_struct("", fields, visitor)
+    }
+}
diff --git a/src/events/attributes.rs b/src/events/attributes.rs
index 286b048..312c37f 100644
--- a/src/events/attributes.rs
+++ b/src/events/attributes.rs
@@ -20,7 +20,7 @@ pub struct Attributes<'a> {
     /// slice of `Element` corresponding to attributes
     bytes: &'a [u8],
     /// current position of the iterator
-    position: usize,
+    pub(crate) position: usize,
     /// if true, checks for duplicate names
     with_checks: bool,
     /// allows attribute without quote or `=`
diff --git a/src/events/mod.rs b/src/events/mod.rs
index 2cad857..f669077 100644
--- a/src/events/mod.rs
+++ b/src/events/mod.rs
@@ -42,7 +42,7 @@ impl<'a> BytesStart<'a> {
     ///
     /// `&content[..name_len]` is not checked to be a valid name
     #[inline]
-    pub fn borrowed(content: &'a [u8], name_len: usize) -> BytesStart<'a> {
+    pub fn borrowed(content: &'a [u8], name_len: usize) -> Self {
         BytesStart {
             buf: Cow::Borrowed(content),
             name_len: name_len,
@@ -87,6 +87,11 @@ impl<'a> BytesStart<'a> {
         Self::owned(self.buf.into_owned(), self.name_len)
     }
 
+    /// Converts the event into an owned event without taking ownershiph of Event
+    pub fn to_owned(&self) -> BytesStart<'static> {
+        Self::owned(self.buf.to_owned(), self.name_len)
+    }
+
     /// Consumes `self` and yield a new `BytesStart` with additional attributes from an iterator.
     ///
     /// The yielded items must be convertible to [`Attribute`] using `Into`.
diff --git a/src/lib.rs b/src/lib.rs
index a5d80b6..d392661 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -114,7 +114,11 @@
 #[cfg(feature = "encoding_rs")]
 extern crate encoding_rs;
 extern crate memchr;
+#[cfg(feature = "serialize")]
+extern crate serde;
 
+#[cfg(feature = "serialize")]
+pub mod de;
 mod errors;
 mod escape;
 pub mod events;
diff --git a/src/reader.rs b/src/reader.rs
index 7f2b91b..516fcc2 100644
--- a/src/reader.rs
+++ b/src/reader.rs
@@ -1,13 +1,13 @@
 //! A module to handle `Reader`
 
-#[cfg(feature = "encoding_rs")]
+#[cfg(feature = "encoding")]
 use std::borrow::Cow;
 use std::fs::File;
 use std::io::{self, BufRead, BufReader};
 use std::path::Path;
 use std::str::from_utf8;
 
-#[cfg(feature = "encoding_rs")]
+#[cfg(feature = "encoding")]
 use encoding_rs::Encoding;
 
 use errors::{Error, Result};
@@ -683,7 +683,7 @@ impl<B: BufRead> Reader<B> {
     /// This encoding will be used by [`decode`].
     ///
     /// [`decode`]: #method.decode
-    #[cfg(feature = "encoding_rs")]
+    #[cfg(feature = "encoding")]
     pub fn encoding(&self) -> &'static Encoding {
         self.encoding
     }
@@ -695,7 +695,7 @@ impl<B: BufRead> Reader<B> {
     ///
     /// If no encoding is specified, defaults to UTF-8.
     #[inline]
-    #[cfg(feature = "encoding_rs")]
+    #[cfg(feature = "encoding")]
     pub fn decode<'b, 'c>(&'b self, bytes: &'c [u8]) -> Cow<'c, str> {
         self.encoding.decode(bytes).0
     }
@@ -709,11 +709,25 @@ impl<B: BufRead> Reader<B> {
     ///
     /// If you instead want to use XML declared encoding, use the `encoding_rs` feature
     #[inline]
-    #[cfg(not(feature = "encoding_rs"))]
+    #[cfg(not(feature = "encoding"))]
     pub fn decode<'c>(&self, bytes: &'c [u8]) -> Result<&'c str> {
         from_utf8(bytes).map_err(Error::Utf8)
     }
 
+    /// Get utf8 decoder
+    #[cfg(feature = "encoding")]
+    pub fn decoder(&self) -> Decoder {
+        Decoder {
+            encoding: self.encoding,
+        }
+    }
+
+    /// Get utf8 decoder
+    #[cfg(not(feature = "encoding"))]
+    pub fn decoder(&self) -> Decoder {
+        Decoder
+    }
+
     /// Reads until end element is found
     ///
     /// Manages nested cases where parent and child elements have the same name
@@ -1141,3 +1155,27 @@ impl NamespaceBufferIndex {
             })
     }
 }
+
+/// Utf8 Decoder
+#[cfg(not(feature = "encoding"))]
+#[derive(Clone, Copy)]
+pub struct Decoder;
+
+/// Utf8 Decoder
+#[cfg(feature = "encoding")]
+#[derive(Clone, Copy)]
+pub struct Decoder {
+    encoding: &'static Encoding,
+}
+
+impl Decoder {
+    #[cfg(not(feature = "encoding"))]
+    pub fn decode<'c>(&self, bytes: &'c [u8]) -> Result<&'c str> {
+        from_utf8(bytes).map_err(Error::Utf8)
+    }
+
+    #[cfg(feature = "encoding")]
+    pub fn decode<'c>(&self, bytes: &'c [u8]) -> Cow<'c, str> {
+        self.encoding.decode(bytes).0
+    }
+}


Test patch:
diff --git a/tests/linescore.xml b/tests/linescore.xml
new file mode 100644
index 0000000..3ab2613
--- /dev/null
+++ b/tests/linescore.xml
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8"?><!--Copyright 2012 MLB Advanced Media, L.P.  Use of any content on this page acknowledges agreement to the terms posted here http://gdx.mlb.com/components/copyright.txt-->
+<game xmlns:xs="http://www.w3.org/2001/XMLSchema" id="2008/07/10/dblrok-ddorok-2"
+      venue="Generic"
+      game_pk="239575"
+      time="Gm 2"
+      time_zone="ET"
+      ampm="AM"
+      away_time="3:33"
+      away_time_zone="ET"
+      away_ampm="AM"
+      home_time="3:33"
+      home_time_zone="ET"
+      home_ampm="AM"
+      game_type="R"
+      tiebreaker_sw="N"
+      original_date="2008/07/10"
+      time_zone_aw_lg="0"
+      time_zone_hm_lg="0"
+      time_aw_lg="3:33"
+      aw_lg_ampm="AM"
+      time_hm_lg="3:33"
+      hm_lg_ampm="AM"
+      venue_id="401"
+      scheduled_innings="7"
+      description="6-13 makeup "
+      away_name_abbrev="DSL BLJ1"
+      home_name_abbrev="DSL DOD"
+      away_code="dbl"
+      away_file_code="t604"
+      away_team_id="604"
+      away_team_city="DSL Blue Jays1"
+      away_team_name="DSL Blue Jays1"
+      away_division="S"
+      away_league_id="130"
+      away_sport_code="rok"
+      home_code="ddo"
+      home_file_code="t611"
+      home_team_id="611"
+      home_team_city="DSL Dodgers"
+      home_team_name="DSL Dodgers"
+      home_division="S"
+      home_league_id="130"
+      home_sport_code="rok"
+      day="THU"
+      gameday_sw="N"
+      double_header_sw="Y"
+      away_games_back="8.5"
+      home_games_back="10.0"
+      venue_w_chan_loc="USNY0996"
+      gameday_link="2008_07_10_dblrok_ddorok_2"
+      away_win="16"
+      away_loss="19"
+      home_win="13"
+      home_loss="22"
+      game_data_directory="/gameday/components/game/rok/gid_2008_07_10_dblrok_ddorok_2"
+      league="DSL"
+      top_inning="N"
+      status="Final"
+      ind="F"
+      inning="7"
+      outs="3"
+      away_team_runs="4"
+      home_team_runs="1"
+      away_team_hits="9"
+      home_team_hits="2"
+      away_team_errors="1"
+      home_team_errors="2"
+      away_team_link="/clubs/index.jsp?cid=t604"
+      home_team_link="/clubs/index.jsp?cid=t611"
+      box_link="/milb/stats/stats.jsp?gid=2008_07_10_dblrok_ddorok_2&amp;t=g_box&amp;did=milb"
+      home_recap_link=""
+      away_recap_link="">
+   <linescore inning="1" home_inning_runs="0" away_inning_runs="1"/>
+   <linescore inning="2" home_inning_runs="0" away_inning_runs="0"/>
+   <linescore inning="3" home_inning_runs="1" away_inning_runs="1"/>
+   <linescore inning="4" home_inning_runs="0" away_inning_runs="2"/>
+   <linescore inning="5" home_inning_runs="0" away_inning_runs="0"/>
+   <linescore inning="6" home_inning_runs="0" away_inning_runs="0"/>
+   <linescore inning="7" home_inning_runs="0" away_inning_runs="0"/>
+   <winning_pitcher first_name="Hector" first="Hector" id="542261" last_name="Velazquez"
+                    last="Velazquez"
+                    name_display_roster=""
+                    wins="4"
+                    losses="0"
+                    era="3.41"
+                    s_wins=""
+                    s_losses=""
+                    s_era=""/>
+   <losing_pitcher first_name="Rubby" first="Rubby" id="523989" last_name="De La Rosa"
+                   last="De La Rosa"
+                   name_display_roster=""
+                   wins="0"
+                   losses="3"
+                   era="1.98"
+                   s_wins=""
+                   s_losses=""
+                   s_era=""/>
+   <save_pitcher first_name="" first="" id="" last_name="" last="" name_display_roster=""
+                 wins="0"
+                 losses="0"
+                 era="0"
+                 s_wins=""
+                 s_losses=""
+                 s_era=""
+                 saves="0"/>
+</game>
+
diff --git a/tests/players.xml b/tests/players.xml
new file mode 100755
index 0000000..c992696
--- /dev/null
+++ b/tests/players.xml
@@ -0,0 +1,56 @@
+<!--Copyright 2015 MLB Advanced Media, L.P.  Use of any content on this page acknowledges agreement to the terms posted here http://gdx.mlb.com/components/copyright.txt--><game venue="Shea Stadium" date="May 10, 2008">
+	<team type="away" id="CIN" name="Cincinnati Reds">
+		<player id="115135" first="Ken" last="Griffey" num="3" boxname="Griffey Jr." rl="L" bats="L" position="RF" current_position="RF" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="" parent_team_id="" bat_order="3" game_position="RF" avg=".244" hr="4" rbi="15"/>
+		<player id="115608" first="Scott" last="Hatteberg" num="21" boxname="Hatteberg" rl="R" bats="L" position="1B" current_position="PH" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="" parent_team_id="" avg=".128" hr="0" rbi="3"/>
+		<player id="118967" first="Kent" last="Mercker" num="50" boxname="Mercker" rl="L" bats="L" position="P" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" avg=".000" hr="0" rbi="0" wins="0" losses="0" era="3.86"/>
+		<player id="136460" first="Alex" last="Gonzalez" num="2" boxname="Gonzalez, Al" rl="R" bats="R" position="SS" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" avg=".000" hr="0" rbi="0"/>
+		<player id="150020" first="Jerry" last="Hairston" num="15" boxname="Hairston, J" rl="R" bats="R" position="SS" current_position="RF" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" avg=".345" hr="1" rbi="5"/>
+		<player id="150188" first="Francisco" last="Cordero" num="48" boxname="Cordero, F" rl="R" bats="R" position="P" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" avg=".000" hr="0" rbi="0" wins="1" losses="0" era="2.08"/>
+		<player id="150221" first="Mike" last="Lincoln" num="57" boxname="Lincoln" rl="R" bats="R" position="P" current_position="P" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="" parent_team_id="" avg=".000" hr="0" rbi="0" wins="0" losses="1" era="3.60"/>
+		<player id="150319" first="Josh" last="Fogg" num="33" boxname="Fogg" rl="R" bats="R" position="P" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" avg=".143" hr="0" rbi="0" wins="1" losses="2" era="9.27"/>
+		<player id="150472" first="Ryan" last="Freel" num="6" boxname="Freel" rl="R" bats="R" position="CF" current_position="LF" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" bat_order="2" game_position="LF" avg=".304" hr="0" rbi="8"/>
+		<player id="276520" first="Bronson" last="Arroyo" num="61" boxname="Arroyo" rl="R" bats="R" position="P" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" avg=".125" hr="0" rbi="0" wins="1" losses="4" era="8.63"/>
+		<player id="279571" first="Matt" last="Belisle" num="31" boxname="Belisle" rl="R" bats="R" position="P" current_position="P" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" bat_order="9" game_position="P" avg=".000" hr="0" rbi="0" wins="1" losses="2" era="6.91"/>
+		<player id="279913" first="Corey" last="Patterson" num="23" boxname="Patterson, C" rl="R" bats="L" position="CF" current_position="CF" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" bat_order="1" game_position="CF" avg=".200" hr="4" rbi="11"/>
+		<player id="346793" first="Jeremy" last="Affeldt" num="41" boxname="Affeldt" rl="L" bats="L" position="P" current_position="P" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="" parent_team_id="" avg=".000" hr="0" rbi="0" wins="1" losses="0" era="3.07"/>
+		<player id="408252" first="Brandon" last="Phillips" num="4" boxname="Phillips" rl="R" bats="R" position="2B" current_position="2B" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" bat_order="4" game_position="2B" avg=".276" hr="6" rbi="16"/>
+		<player id="421685" first="Aaron" last="Harang" num="39" boxname="Harang" rl="R" bats="R" position="P" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" avg=".235" hr="0" rbi="0" wins="1" losses="5" era="3.09"/>
+		<player id="424325" first="David" last="Ross" num="26" boxname="Ross, D" rl="R" bats="R" position="C" current_position="C" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="" parent_team_id="" bat_order="8" game_position="C" avg=".167" hr="0" rbi="0"/>
+		<player id="429665" first="Edwin" last="Encarnacion" num="28" boxname="Encarnacion" rl="R" bats="R" position="3B" current_position="3B" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" bat_order="6" game_position="3B" avg=".258" hr="7" rbi="16"/>
+		<player id="433898" first="Jeff" last="Keppinger" num="27" boxname="Keppinger" rl="R" bats="R" position="SS" current_position="SS" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" bat_order="7" game_position="SS" avg=".292" hr="2" rbi="16"/>
+		<player id="435538" first="Bill" last="Bray" num="45" boxname="Bray" rl="L" bats="L" position="P" current_position="P" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" avg=".000" hr="0" rbi="0" wins="0" losses="0" era="0.00"/>
+		<player id="440361" first="Norris" last="Hopper" num="30" boxname="Hopper" rl="R" bats="R" position="O" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" avg=".240" hr="0" rbi="0"/>
+		<player id="450172" first="Edinson" last="Volquez" num="36" boxname="Volquez" rl="R" bats="R" position="P" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" avg=".133" hr="0" rbi="1" wins="5" losses="1" era="1.06"/>
+		<player id="454537" first="Jared" last="Burton" num="51" boxname="Burton" rl="R" bats="R" position="P" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" avg=".000" hr="0" rbi="0" wins="1" losses="1" era="4.02"/>
+		<player id="455751" first="Bobby" last="Livingston" num="37" boxname="Livingston" rl="L" bats="L" position="P" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" avg=".000" hr="0" rbi="0" wins="0" losses="0" era="-"/>
+		<player id="456501" first="Johnny" last="Cueto" num="47" boxname="Cueto" rl="R" bats="R" position="P" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" avg=".000" hr="0" rbi="0" wins="2" losses="3" era="5.27"/>
+		<player id="458015" first="Joey" last="Votto" num="19" boxname="Votto" rl="R" bats="L" position="1B" current_position="1B" status="A" team_abbrev="CIN" team_id="113" parent_team_abbrev="CIN" parent_team_id="113" bat_order="5" game_position="1B" avg=".302" hr="7" rbi="18"/>
+<coach position="manager" first="Dusty" last="Baker" id="110481" num="12"/><coach position="batting_coach" first="Brook" last="Jacoby" id="116461" num="49"/><coach position="pitching_coach" first="Dick" last="Pole" id="120649" num="43"/><coach position="first_base_coach" first="Billy" last="Hatcher" id="115602" num="22"/><coach position="third_base_coach" first="Mark" last="Berry" id="427028" num="41"/><coach position="bench_coach" first="Chris" last="Speier" id="122573" num="35"/><coach position="bullpen_coach" first="Juan" last="Lopez" id="427306" num="59"/><coach position="bullpen_catcher" first="Mike" last="Stefanski" id="150464" num="72"/>	</team>
+	<team type="home" id="NYM" name="New York Mets">
+		<player id="110189" first="Moises" last="Alou" num="18" boxname="Alou" rl="R" bats="R" position="LF" current_position="LF" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" bat_order="6" game_position="LF" avg=".250" hr="0" rbi="1"/>
+		<player id="112116" first="Luis" last="Castillo" num="1" boxname="Castillo, L" rl="R" bats="S" position="2B" current_position="2B" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" bat_order="2" game_position="2B" avg=".242" hr="0" rbi="7"/>
+		<player id="113232" first="Carlos" last="Delgado" num="21" boxname="Delgado" rl="R" bats="L" position="1B" current_position="1B" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" bat_order="7" game_position="1B" avg=".216" hr="4" rbi="16"/>
+		<player id="113702" first="Damion" last="Easley" num="3" boxname="Easley" rl="R" bats="R" position="2B" current_position="PH" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="" parent_team_id="" avg=".262" hr="0" rbi="4"/>
+		<player id="118377" first="Pedro" last="Martinez" num="45" boxname="Martinez, P" rl="R" bats="R" position="P" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" avg=".000" hr="0" rbi="0" wins="0" losses="0" era="10.80"/>
+		<player id="123790" first="Billy" last="Wagner" num="13" boxname="Wagner, B" rl="L" bats="L" position="P" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" avg=".000" hr="0" rbi="0" wins="0" losses="0" era="0.00"/>
+		<player id="133340" first="Orlando" last="Hernandez" num="26" boxname="Hernandez, O" rl="R" bats="R" position="P" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" avg=".000" hr="0" rbi="0" wins="0" losses="0" era="-"/>
+		<player id="135783" first="Ramon" last="Castro" num="11" boxname="Castro, R" rl="R" bats="R" position="C" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" avg=".000" hr="0" rbi="0"/>
+		<player id="136724" first="Marlon" last="Anderson" num="9" boxname="Anderson" rl="R" bats="L" position="LF" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" avg=".214" hr="1" rbi="3"/>
+		<player id="136860" first="Carlos" last="Beltran" num="15" boxname="Beltran" rl="R" bats="S" position="CF" current_position="CF" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" bat_order="4" game_position="CF" avg=".218" hr="2" rbi="13"/>
+		<player id="150411" first="Brian" last="Schneider" num="23" boxname="Schneider" rl="R" bats="L" position="C" current_position="C" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" bat_order="8" game_position="C" avg=".308" hr="0" rbi="8"/>
+		<player id="276371" first="Johan" last="Santana" num="57" boxname="Santana, J" rl="L" bats="L" position="P" current_position="P" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" bat_order="9" game_position="P" avg=".250" hr="0" rbi="0" wins="3" losses="2" era="2.91"/>
+		<player id="277184" first="Matt" last="Wise" num="38" boxname="Wise" rl="R" bats="R" position="P" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" avg=".000" hr="0" rbi="0" wins="0" losses="1" era="9.00"/>
+		<player id="346795" first="Endy" last="Chavez" num="10" boxname="Chavez, En" rl="L" bats="L" position="RF" current_position="LF" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="" parent_team_id="" avg=".179" hr="0" rbi="0"/>
+		<player id="407901" first="Jorge" last="Sosa" num="29" boxname="Sosa" rl="R" bats="R" position="P" current_position="P" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="" parent_team_id="" avg=".000" hr="0" rbi="0" wins="4" losses="1" era="6.05"/>
+		<player id="408230" first="Pedro" last="Feliciano" num="25" boxname="Feliciano, P" rl="L" bats="L" position="P" current_position="P" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" avg=".000" hr="0" rbi="0" wins="0" losses="1" era="0.84"/>
+		<player id="408310" first="Aaron" last="Heilman" num="48" boxname="Heilman" rl="R" bats="R" position="P" current_position="P" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="" parent_team_id="" avg=".000" hr="0" rbi="0" wins="0" losses="0" era="4.66"/>
+		<player id="408314" first="Jose" last="Reyes" num="7" boxname="Reyes" rl="R" bats="S" position="SS" current_position="SS" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" bat_order="1" game_position="SS" avg=".254" hr="2" rbi="13"/>
+		<player id="425508" first="Ryan" last="Church" num="19" boxname="Church" rl="L" bats="L" position="RF" current_position="RF" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" bat_order="5" game_position="RF" avg=".328" hr="6" rbi="24"/>
+		<player id="429720" first="John" last="Maine" num="33" boxname="Maine" rl="R" bats="R" position="P" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" avg=".067" hr="0" rbi="2" wins="4" losses="2" era="3.00"/>
+		<player id="431151" first="David" last="Wright" num="5" boxname="Wright, D" rl="R" bats="R" position="3B" current_position="3B" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" bat_order="3" game_position="3B" avg=".262" hr="6" rbi="28"/>
+		<player id="434586" first="Ambiorix" last="Burgos" num="40" boxname="Burgos, A" rl="R" bats="R" position="P" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" avg=".000" hr="0" rbi="0" wins="0" losses="0" era="-"/>
+		<player id="434636" first="Angel" last="Pagan" num="16" boxname="Pagan" rl="R" bats="S" position="LF" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" avg=".281" hr="0" rbi="13"/>
+		<player id="450306" first="Jason" last="Vargas" num="39" boxname="Vargas" rl="L" bats="L" position="P" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" avg=".000" hr="0" rbi="0" wins="0" losses="0" era="-"/>
+		<player id="460059" first="Mike" last="Pelfrey" num="34" boxname="Pelfrey" rl="R" bats="R" position="P" status="A" team_abbrev="NYM" team_id="121" parent_team_abbrev="NYM" parent_team_id="121" avg=".000" hr="0" rbi="0" wins="2" losses="2" era="5.27"/>
+<coach position="manager" first="Willie" last="Randolph" id="120927" num="12"/><coach position="batting_coach" first="Howard" last="Johnson" id="116593" num="20"/><coach position="pitching_coach" first="Rick" last="Peterson" id="427395" num="51"/><coach position="first_base_coach" first="Tom" last="Nieto" id="119796" num="55"/><coach position="third_base_coach" first="Sandy" last="Alomar" id="110185" num="2"/><coach position="bench_coach" first="Jerry" last="Manuel" id="118262" num="53"/><coach position="bullpen_coach" first="Guy" last="Conti" id="434699" num="52"/><coach position="bullpen_catcher" first="Dave" last="Racaniello" id="534948" num="54"/><coach position="coach" first="Sandy" last="Alomar" id="110184" num="58"/><coach position="coach" first="Juan" last="Lopez" id="495390" num="56"/>	</team>
+<umpires><umpire position="home" name="Ron Kulpa" id="427292"/><umpire position="first" name="Damien Beal" id="482483"/><umpire position="second" name="Dale Scott" id="427457"/><umpire position="third" name="Dan Iassogna" id="427248"/></umpires></game>
\ No newline at end of file
diff --git a/tests/test.rs b/tests/test.rs
index 2ea012f..aeefa2b 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -1,4 +1,6 @@
 extern crate quick_xml;
+#[cfg(feature = "serialize")]
+extern crate serde;
 
 use quick_xml::events::attributes::Attribute;
 use quick_xml::events::Event::*;
@@ -6,6 +8,9 @@ use quick_xml::Reader;
 use std::borrow::Cow;
 use std::io::Cursor;
 
+#[cfg(feature = "serialize")]
+use serde::{Deserialize, Serialize};
+
 #[test]
 fn test_sample() {
     let src: &[u8] = include_bytes!("sample_rss.xml");
@@ -453,3 +458,686 @@ fn test_default_namespace() {
         panic!("expecting outer end element with no namespace");
     }
 }
+
+#[cfg(feature = "serialize")]
+#[test]
+fn line_score() {
+    #[derive(Debug, PartialEq, Deserialize)]
+    struct LineScoreData {
+        game_pk: u32,
+        game_type: char,
+        venue: String,
+        venue_w_chan_loc: String,
+        venue_id: u32,
+        time: String,
+        time_zone: String,
+        ampm: String,
+        home_team_id: u32,
+        home_team_city: String,
+        home_team_name: String,
+        home_league_id: u32,
+        away_team_id: u32,
+        away_team_city: String,
+        away_team_name: String,
+        away_league_id: u32,
+        #[serde(rename = "linescore", skip_serializing)]
+        innings: Vec<LineScore>,
+    }
+    #[derive(Debug, PartialEq, Deserialize)]
+    struct LineScore {
+        #[serde(rename = "away_inning_runs")]
+        away_runs: u32,
+        #[serde(rename = "home_inning_runs")]
+        //needs to be an Option, since home team doesn't always bat.
+        home_runs: Option<u32>,
+        // Keeping the inning as a string, since we'll need it to construct URLs later
+        inning: String,
+    }
+
+    let res: LineScoreData = quick_xml::de::from_str(include_str!("linescore.xml")).unwrap();
+
+    let expected = LineScoreData {
+        game_pk: 239575,
+        game_type: 'R',
+        venue: "Generic".to_owned(),
+        venue_w_chan_loc: "USNY0996".to_owned(),
+        venue_id: 401,
+        time: "Gm 2".to_owned(),
+        time_zone: "ET".to_owned(),
+        ampm: "AM".to_owned(),
+        home_team_id: 611,
+        home_team_city: "DSL Dodgers".to_owned(),
+        home_team_name: "DSL Dodgers".to_owned(),
+        home_league_id: 130,
+        away_team_id: 604,
+        away_team_city: "DSL Blue Jays1".to_owned(),
+        away_team_name: "DSL Blue Jays1".to_owned(),
+        away_league_id: 130,
+        innings: vec![
+            LineScore {
+                away_runs: 1,
+                home_runs: Some(0),
+                inning: "1".to_owned(),
+            },
+            LineScore {
+                away_runs: 0,
+                home_runs: Some(0),
+                inning: "2".to_owned(),
+            },
+            LineScore {
+                away_runs: 1,
+                home_runs: Some(1),
+                inning: "3".to_owned(),
+            },
+            LineScore {
+                away_runs: 2,
+                home_runs: Some(0),
+                inning: "4".to_owned(),
+            },
+            LineScore {
+                away_runs: 0,
+                home_runs: Some(0),
+                inning: "5".to_owned(),
+            },
+            LineScore {
+                away_runs: 0,
+                home_runs: Some(0),
+                inning: "6".to_owned(),
+            },
+            LineScore {
+                away_runs: 0,
+                home_runs: Some(0),
+                inning: "7".to_owned(),
+            },
+        ],
+    };
+    assert_eq!(res, expected);
+}
+
+#[cfg(feature = "serialize")]
+#[test]
+fn players() {
+    #[derive(PartialEq, Deserialize, Serialize, Debug)]
+    struct Game {
+        #[serde(rename = "team")]
+        teams: Vec<Team>,
+        //umpires: Umpires
+    }
+
+    #[derive(PartialEq, Deserialize, Serialize, Debug)]
+    struct Team {
+        #[serde(rename = "type")]
+        home_away: HomeAway,
+        id: String,
+        name: String,
+        #[serde(rename = "player")]
+        players: Vec<Player>,
+        #[serde(rename = "coach")]
+        coaches: Vec<Coach>,
+    }
+
+    #[derive(PartialEq, Deserialize, Serialize, Debug)]
+    enum HomeAway {
+        #[serde(rename = "home")]
+        Home,
+        #[serde(rename = "away")]
+        Away,
+    }
+
+    #[derive(PartialEq, Deserialize, Serialize, Debug, Clone)]
+    struct Player {
+        id: u32,
+        #[serde(rename = "first")]
+        name_first: String,
+        #[serde(rename = "last")]
+        name_last: String,
+        game_position: Option<String>,
+        bat_order: Option<u8>,
+        position: String,
+    }
+
+    #[derive(PartialEq, Deserialize, Serialize, Debug)]
+    struct Coach {
+        position: String,
+        #[serde(rename = "first")]
+        name_first: String,
+        #[serde(rename = "last")]
+        name_last: String,
+        id: u32,
+    }
+
+    let res: Game = quick_xml::de::from_str(include_str!("players.xml")).unwrap();
+
+    let expected = Game {
+        teams: vec![
+            Team {
+                home_away: HomeAway::Away,
+                id: "CIN".to_owned(),
+                name: "Cincinnati Reds".to_owned(),
+                players: vec![
+                    Player {
+                        id: 115135,
+                        name_first: "Ken".to_owned(),
+                        name_last: "Griffey".to_owned(),
+                        game_position: Some("RF".to_owned()),
+                        bat_order: Some(3),
+                        position: "RF".to_owned(),
+                    },
+                    Player {
+                        id: 115608,
+                        name_first: "Scott".to_owned(),
+                        name_last: "Hatteberg".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "1B".to_owned(),
+                    },
+                    Player {
+                        id: 118967,
+                        name_first: "Kent".to_owned(),
+                        name_last: "Mercker".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 136460,
+                        name_first: "Alex".to_owned(),
+                        name_last: "Gonzalez".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "SS".to_owned(),
+                    },
+                    Player {
+                        id: 150020,
+                        name_first: "Jerry".to_owned(),
+                        name_last: "Hairston".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "SS".to_owned(),
+                    },
+                    Player {
+                        id: 150188,
+                        name_first: "Francisco".to_owned(),
+                        name_last: "Cordero".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 150221,
+                        name_first: "Mike".to_owned(),
+                        name_last: "Lincoln".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 150319,
+                        name_first: "Josh".to_owned(),
+                        name_last: "Fogg".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 150472,
+                        name_first: "Ryan".to_owned(),
+                        name_last: "Freel".to_owned(),
+                        game_position: Some("LF".to_owned()),
+                        bat_order: Some(2),
+                        position: "CF".to_owned(),
+                    },
+                    Player {
+                        id: 276520,
+                        name_first: "Bronson".to_owned(),
+                        name_last: "Arroyo".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 279571,
+                        name_first: "Matt".to_owned(),
+                        name_last: "Belisle".to_owned(),
+                        game_position: Some("P".to_owned()),
+                        bat_order: Some(9),
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 279913,
+                        name_first: "Corey".to_owned(),
+                        name_last: "Patterson".to_owned(),
+                        game_position: Some("CF".to_owned()),
+                        bat_order: Some(1),
+                        position: "CF".to_owned(),
+                    },
+                    Player {
+                        id: 346793,
+                        name_first: "Jeremy".to_owned(),
+                        name_last: "Affeldt".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 408252,
+                        name_first: "Brandon".to_owned(),
+                        name_last: "Phillips".to_owned(),
+                        game_position: Some("2B".to_owned()),
+                        bat_order: Some(4),
+                        position: "2B".to_owned(),
+                    },
+                    Player {
+                        id: 421685,
+                        name_first: "Aaron".to_owned(),
+                        name_last: "Harang".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 424325,
+                        name_first: "David".to_owned(),
+                        name_last: "Ross".to_owned(),
+                        game_position: Some("C".to_owned()),
+                        bat_order: Some(8),
+                        position: "C".to_owned(),
+                    },
+                    Player {
+                        id: 429665,
+                        name_first: "Edwin".to_owned(),
+                        name_last: "Encarnacion".to_owned(),
+                        game_position: Some("3B".to_owned()),
+                        bat_order: Some(6),
+                        position: "3B".to_owned(),
+                    },
+                    Player {
+                        id: 433898,
+                        name_first: "Jeff".to_owned(),
+                        name_last: "Keppinger".to_owned(),
+                        game_position: Some("SS".to_owned()),
+                        bat_order: Some(7),
+                        position: "SS".to_owned(),
+                    },
+                    Player {
+                        id: 435538,
+                        name_first: "Bill".to_owned(),
+                        name_last: "Bray".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 440361,
+                        name_first: "Norris".to_owned(),
+                        name_last: "Hopper".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "O".to_owned(),
+                    },
+                    Player {
+                        id: 450172,
+                        name_first: "Edinson".to_owned(),
+                        name_last: "Volquez".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 454537,
+                        name_first: "Jared".to_owned(),
+                        name_last: "Burton".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 455751,
+                        name_first: "Bobby".to_owned(),
+                        name_last: "Livingston".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 456501,
+                        name_first: "Johnny".to_owned(),
+                        name_last: "Cueto".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 458015,
+                        name_first: "Joey".to_owned(),
+                        name_last: "Votto".to_owned(),
+                        game_position: Some("1B".to_owned()),
+                        bat_order: Some(5),
+                        position: "1B".to_owned(),
+                    },
+                ],
+                coaches: vec![
+                    Coach {
+                        position: "manager".to_owned(),
+                        name_first: "Dusty".to_owned(),
+                        name_last: "Baker".to_owned(),
+                        id: 110481,
+                    },
+                    Coach {
+                        position: "batting_coach".to_owned(),
+                        name_first: "Brook".to_owned(),
+                        name_last: "Jacoby".to_owned(),
+                        id: 116461,
+                    },
+                    Coach {
+                        position: "pitching_coach".to_owned(),
+                        name_first: "Dick".to_owned(),
+                        name_last: "Pole".to_owned(),
+                        id: 120649,
+                    },
+                    Coach {
+                        position: "first_base_coach".to_owned(),
+                        name_first: "Billy".to_owned(),
+                        name_last: "Hatcher".to_owned(),
+                        id: 115602,
+                    },
+                    Coach {
+                        position: "third_base_coach".to_owned(),
+                        name_first: "Mark".to_owned(),
+                        name_last: "Berry".to_owned(),
+                        id: 427028,
+                    },
+                    Coach {
+                        position: "bench_coach".to_owned(),
+                        name_first: "Chris".to_owned(),
+                        name_last: "Speier".to_owned(),
+                        id: 122573,
+                    },
+                    Coach {
+                        position: "bullpen_coach".to_owned(),
+                        name_first: "Juan".to_owned(),
+                        name_last: "Lopez".to_owned(),
+                        id: 427306,
+                    },
+                    Coach {
+                        position: "bullpen_catcher".to_owned(),
+                        name_first: "Mike".to_owned(),
+                        name_last: "Stefanski".to_owned(),
+                        id: 150464,
+                    },
+                ],
+            },
+            Team {
+                home_away: HomeAway::Home,
+                id: "NYM".to_owned(),
+                name: "New York Mets".to_owned(),
+                players: vec![
+                    Player {
+                        id: 110189,
+                        name_first: "Moises".to_owned(),
+                        name_last: "Alou".to_owned(),
+                        game_position: Some("LF".to_owned()),
+                        bat_order: Some(6),
+                        position: "LF".to_owned(),
+                    },
+                    Player {
+                        id: 112116,
+                        name_first: "Luis".to_owned(),
+                        name_last: "Castillo".to_owned(),
+                        game_position: Some("2B".to_owned()),
+                        bat_order: Some(2),
+                        position: "2B".to_owned(),
+                    },
+                    Player {
+                        id: 113232,
+                        name_first: "Carlos".to_owned(),
+                        name_last: "Delgado".to_owned(),
+                        game_position: Some("1B".to_owned()),
+                        bat_order: Some(7),
+                        position: "1B".to_owned(),
+                    },
+                    Player {
+                        id: 113702,
+                        name_first: "Damion".to_owned(),
+                        name_last: "Easley".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "2B".to_owned(),
+                    },
+                    Player {
+                        id: 118377,
+                        name_first: "Pedro".to_owned(),
+                        name_last: "Martinez".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 123790,
+                        name_first: "Billy".to_owned(),
+                        name_last: "Wagner".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 133340,
+                        name_first: "Orlando".to_owned(),
+                        name_last: "Hernandez".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 135783,
+                        name_first: "Ramon".to_owned(),
+                        name_last: "Castro".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "C".to_owned(),
+                    },
+                    Player {
+                        id: 136724,
+                        name_first: "Marlon".to_owned(),
+                        name_last: "Anderson".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "LF".to_owned(),
+                    },
+                    Player {
+                        id: 136860,
+                        name_first: "Carlos".to_owned(),
+                        name_last: "Beltran".to_owned(),
+                        game_position: Some("CF".to_owned()),
+                        bat_order: Some(4),
+                        position: "CF".to_owned(),
+                    },
+                    Player {
+                        id: 150411,
+                        name_first: "Brian".to_owned(),
+                        name_last: "Schneider".to_owned(),
+                        game_position: Some("C".to_owned()),
+                        bat_order: Some(8),
+                        position: "C".to_owned(),
+                    },
+                    Player {
+                        id: 276371,
+                        name_first: "Johan".to_owned(),
+                        name_last: "Santana".to_owned(),
+                        game_position: Some("P".to_owned()),
+                        bat_order: Some(9),
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 277184,
+                        name_first: "Matt".to_owned(),
+                        name_last: "Wise".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 346795,
+                        name_first: "Endy".to_owned(),
+                        name_last: "Chavez".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "RF".to_owned(),
+                    },
+                    Player {
+                        id: 407901,
+                        name_first: "Jorge".to_owned(),
+                        name_last: "Sosa".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 408230,
+                        name_first: "Pedro".to_owned(),
+                        name_last: "Feliciano".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 408310,
+                        name_first: "Aaron".to_owned(),
+                        name_last: "Heilman".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 408314,
+                        name_first: "Jose".to_owned(),
+                        name_last: "Reyes".to_owned(),
+                        game_position: Some("SS".to_owned()),
+                        bat_order: Some(1),
+                        position: "SS".to_owned(),
+                    },
+                    Player {
+                        id: 425508,
+                        name_first: "Ryan".to_owned(),
+                        name_last: "Church".to_owned(),
+                        game_position: Some("RF".to_owned()),
+                        bat_order: Some(5),
+                        position: "RF".to_owned(),
+                    },
+                    Player {
+                        id: 429720,
+                        name_first: "John".to_owned(),
+                        name_last: "Maine".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 431151,
+                        name_first: "David".to_owned(),
+                        name_last: "Wright".to_owned(),
+                        game_position: Some("3B".to_owned()),
+                        bat_order: Some(3),
+                        position: "3B".to_owned(),
+                    },
+                    Player {
+                        id: 434586,
+                        name_first: "Ambiorix".to_owned(),
+                        name_last: "Burgos".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 434636,
+                        name_first: "Angel".to_owned(),
+                        name_last: "Pagan".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "LF".to_owned(),
+                    },
+                    Player {
+                        id: 450306,
+                        name_first: "Jason".to_owned(),
+                        name_last: "Vargas".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                    Player {
+                        id: 460059,
+                        name_first: "Mike".to_owned(),
+                        name_last: "Pelfrey".to_owned(),
+                        game_position: None,
+                        bat_order: None,
+                        position: "P".to_owned(),
+                    },
+                ],
+                coaches: vec![
+                    Coach {
+                        position: "manager".to_owned(),
+                        name_first: "Willie".to_owned(),
+                        name_last: "Randolph".to_owned(),
+                        id: 120927,
+                    },
+                    Coach {
+                        position: "batting_coach".to_owned(),
+                        name_first: "Howard".to_owned(),
+                        name_last: "Johnson".to_owned(),
+                        id: 116593,
+                    },
+                    Coach {
+                        position: "pitching_coach".to_owned(),
+                        name_first: "Rick".to_owned(),
+                        name_last: "Peterson".to_owned(),
+                        id: 427395,
+                    },
+                    Coach {
+                        position: "first_base_coach".to_owned(),
+                        name_first: "Tom".to_owned(),
+                        name_last: "Nieto".to_owned(),
+                        id: 119796,
+                    },
+                    Coach {
+                        position: "third_base_coach".to_owned(),
+                        name_first: "Sandy".to_owned(),
+                        name_last: "Alomar".to_owned(),
+                        id: 110185,
+                    },
+                    Coach {
+                        position: "bench_coach".to_owned(),
+                        name_first: "Jerry".to_owned(),
+                        name_last: "Manuel".to_owned(),
+                        id: 118262,
+                    },
+                    Coach {
+                        position: "bullpen_coach".to_owned(),
+                        name_first: "Guy".to_owned(),
+                        name_last: "Conti".to_owned(),
+                        id: 434699,
+                    },
+                    Coach {
+                        position: "bullpen_catcher".to_owned(),
+                        name_first: "Dave".to_owned(),
+                        name_last: "Racaniello".to_owned(),
+                        id: 534948,
+                    },
+                    Coach {
+                        position: "coach".to_owned(),
+                        name_first: "Sandy".to_owned(),
+                        name_last: "Alomar".to_owned(),
+                        id: 110184,
+                    },
+                    Coach {
+                        position: "coach".to_owned(),
+                        name_first: "Juan".to_owned(),
+                        name_last: "Lopez".to_owned(),
+                        id: 495390,
+                    },
+                ],
+            },
+        ],
+    };
+
+    assert_eq!(res, expected);
+}
diff --git a/tests/unit_tests.rs b/tests/unit_tests.rs
index 85c744f..56e52d3 100644
--- a/tests/unit_tests.rs
+++ b/tests/unit_tests.rs
@@ -425,7 +425,7 @@ fn test_buf_position() {
 #[test]
 fn test_namespace() {
     let mut r = Reader::from_str("<a xmlns:myns='www1'><myns:b>in namespace!</myns:b></a>");
-    r.trim_text(true);;
+    r.trim_text(true);
 
     let mut buf = Vec::new();
     let mut ns_buf = Vec::new();
@@ -448,7 +448,7 @@ fn test_namespace() {
 #[test]
 fn test_default_namespace() {
     let mut r = Reader::from_str("<a ><b xmlns=\"www1\"></b></a>");
-    r.trim_text(true);;
+    r.trim_text(true);
 
     // <a>
     let mut buf = Vec::new();
@@ -491,7 +491,7 @@ fn test_default_namespace() {
 #[test]
 fn test_default_namespace_reset() {
     let mut r = Reader::from_str("<a xmlns=\"www1\"><b xmlns=\"\"></b></a>");
-    r.trim_text(true);;
+    r.trim_text(true);
 
     let mut buf = Vec::new();
     let mut ns_buf = Vec::new();