diff --git a/src/config.rs b/src/config.rs
index 7616575..a1fee6e 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,7 +1,6 @@
 use std::{collections::HashSet, path::PathBuf, time::Duration};
 
 use anyhow::{Error, Result};
-use quick_xml::{events::Event, Reader};
 use serde::Deserialize;
 
 mod xml;
@@ -11,24 +10,13 @@ use xml::{
     Document, Element, PolicyContext, PolicyElement, RuleAttributes, RuleElement, TypeElement,
 };
 
-const EXPECTED_DOCTYPE_PARTS: &[&str] = &[
-    "busconfig",
-    "PUBLIC",
-    r#""-//freedesktop//DTD"#,
-    "D-Bus",
-    "Bus",
-    "Configuration",
-    r#"1.0//EN""#,
-    r#""http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd""#,
-];
-
 const STANDARD_SYSTEM_SERVICEDIRS: &[&str] = &[
     "/usr/local/share/dbus-1/system-services",
     "/usr/share/dbus-1/system-services",
     "/lib/dbus-1/system-services",
 ];
 
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, Deserialize, PartialEq)]
 pub enum Access {
     Allow,
     Deny,
@@ -41,7 +29,6 @@ pub enum Access {
 ///
 /// [XML configuration files]: https://dbus.freedesktop.org/doc/dbus-daemon.1.html#configuration_file
 #[derive(Clone, Debug, Default, Deserialize, PartialEq)]
-#[serde(try_from = "Document")]
 pub struct BusConfig {
     /// If `true`, connections that authenticated using the ANONYMOUS mechanism will be authorized
     /// to connect. This option has no practical effect unless the ANONYMOUS mechanism has also
@@ -213,43 +200,9 @@ impl BusConfig {
     }
 
     pub fn parse(s: &str) -> Result<Self> {
-        // validate that our DOCTYPE and root element are correct
-        let mut reader = Reader::from_reader(s.as_bytes());
-        let mut has_dtd = false;
-        loop {
-            match reader.read_event()? {
-                Event::DocType(bytes_text) => {
-                    has_dtd = true;
-                    let content = bytes_text.unescape()?;
-                    // this approach does throw away extra whitespace within quoted strings,
-                    // which means we do allow through some unexpected DOCTYPEs
-                    // TODO: consider whether added complexity is worth 100% strictness for DOCTYPEs
-                    let dtd_parts: Vec<&str> = content.as_ref().split_whitespace().collect();
-                    if dtd_parts != EXPECTED_DOCTYPE_PARTS {
-                        assert_eq!(dtd_parts, EXPECTED_DOCTYPE_PARTS);
-                        return Err(Error::msg(
-                            "incorrect/incomplete `<!DOCTYPE ...> declaration`",
-                        ));
-                    }
-                    // we currently don't bother with the case of multiple DTDs
-                }
-                Event::Start(bytes_start) => {
-                    if !has_dtd {
-                        return Err(Error::msg(
-                            "must specify `<!DOCTYPE ...>` before root element",
-                        ));
-                    }
-                    if bytes_start.name().as_ref() != b"busconfig" {
-                        return Err(Error::msg("root element must be `<busconfig>`"));
-                    }
-                    break;
-                }
-                Event::Eof => break,
-                _ => {}
-            }
-        }
-
-        quick_xml::de::from_str(s).map_err(Error::from)
+        // TODO: efficiently validate that our DOCTYPE and root element are correct
+        let doc: Document = quick_xml::de::from_str(s)?;
+        Self::try_from(doc)
     }
 }
 
@@ -310,7 +263,7 @@ pub enum MessageType {
     Error,
 }
 
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, Deserialize, PartialEq)]
 pub enum Operation {
     /// rules checked when a new connection to the message bus is established
     Connect,
@@ -376,7 +329,7 @@ impl TryFrom<RuleAttributes> for OptionalOperation {
     }
 }
 
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, Deserialize, PartialEq)]
 pub enum Policy {
     DefaultContext(Vec<Rule>),
     Group(Vec<Rule>, String),
@@ -442,7 +395,7 @@ impl TryFrom<PolicyElement> for OptionalPolicy {
     }
 }
 
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, Deserialize, PartialEq)]
 pub struct ReceiveOperation {
     pub error: String,
     pub interface: String,
@@ -584,7 +537,7 @@ fn rules_try_from_rule_elements(value: Vec<RuleElement>) -> Result<Vec<Rule>> {
 
 pub type Rule = (Access, Operation);
 
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, Deserialize, PartialEq)]
 pub struct SendOperation {
     pub broadcast: Option<bool>,
     pub destination: String,
@@ -631,34 +584,6 @@ impl From<RuleAttributes> for SendOperation {
 mod tests {
     use super::*;
 
-    #[test]
-    #[should_panic]
-    fn bus_config_from_str_without_dtd_error() {
-        let input = r#"<busconfig></busconfig>"#;
-        BusConfig::parse(input).expect("should parse XML input");
-    }
-
-    #[test]
-    #[should_panic]
-    fn bus_config_parse_with_unexpected_dtd_error() {
-        let input = r#"<!DOCTYPE busconfig PUBLIC
-        "-//W3C//DTD XHTML Basic 1.1//EN"
-        "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">
-        <busconfig></busconfig>
-        "#;
-        BusConfig::parse(input).expect("should parse XML input");
-    }
-
-    #[test]
-    #[should_panic]
-    fn bus_config_parse_with_unexpected_root_element_error() {
-        let input = r#"<!DOCTYPE foo PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
-        "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
-        <foo></foo>
-        "#;
-        BusConfig::parse(input).expect("should parse XML input");
-    }
-
     #[test]
     fn bus_config_parse_with_dtd_and_root_element_ok() {
         let input = r#"<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"