diff --git a/Cargo.lock b/Cargo.lock
index 6048d3f4e6e8acea5790db1a4a79260ad6836684..5ec9f0ef93fc3414be6f7afaf572066491904725 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -871,8 +871,7 @@ dependencies = [
 [[package]]
 name = "derive_builder_core"
 version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5"
+source = "git+https://github.com/ijackson/rust-derive-builder?rev=dccbbb8ad75717c8bc0070b6f0364b2c3a54abb7#dccbbb8ad75717c8bc0070b6f0364b2c3a54abb7"
 dependencies = [
  "darling",
  "proc-macro2",
@@ -3099,6 +3098,8 @@ name = "tor-basic-utils"
 version = "0.1.0"
 dependencies = [
  "educe",
+ "humantime-serde",
+ "serde",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index c88138e9f92de1aec82b8d332f163ee751b5011c..2695a05bdd4e868b289f6bcc2630e9d143069b50 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -61,3 +61,7 @@ codegen-units = 1
 # Optimize for size.  [Actually this is even smaller than 'z' on rust
 # 1.56.  It saves about 11% download size over the default value of '3'.]
 opt-level = 's'
+
+[patch.crates-io.derive_builder_core]
+git = "https://github.com/ijackson/rust-derive-builder"
+rev = "dccbbb8ad75717c8bc0070b6f0364b2c3a54abb7"
diff --git a/crates/arti-client/Cargo.toml b/crates/arti-client/Cargo.toml
index f304eb5f1fb75ee2d3135a66cc9c4caefa2d208c..d8a556681c920b031b0dc7633e61c64985f875c3 100644
--- a/crates/arti-client/Cargo.toml
+++ b/crates/arti-client/Cargo.toml
@@ -39,7 +39,7 @@ tor-proto = { path="../tor-proto", version = "0.1.0"}
 tor-rtcompat = { path="../tor-rtcompat", version = "0.1.0"}
 
 humantime-serde = "1"
-derive_builder = "0.10"
+derive_builder = "0.10.2"
 derive_more = "0.99"
 directories = "4"
 educe = "0.4.6"
diff --git a/crates/arti-client/src/config.rs b/crates/arti-client/src/config.rs
index e2af900cfcde1a07a45ef1791fd3b438f56c7113..7459ac6694e7ccdd180d9c681a8fe1ba1271a5ab 100644
--- a/crates/arti-client/src/config.rs
+++ b/crates/arti-client/src/config.rs
@@ -17,6 +17,7 @@ use std::collections::HashMap;
 use std::path::Path;
 use std::path::PathBuf;
 use std::time::Duration;
+pub use tor_basic_utils::humantime_serde_option;
 pub use tor_config::{CfgPath, ConfigBuildError, Reconfigure};
 
 /// Types for configuring how Tor circuits are built.
@@ -46,6 +47,7 @@ pub mod dir {
 /// and requests.
 #[derive(Debug, Clone, Builder, Deserialize, Eq, PartialEq)]
 #[builder(build_fn(error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 #[serde(deny_unknown_fields)]
 pub struct ClientAddrConfig {
     /// Should we allow attempts to make Tor connections to local addresses?
@@ -67,6 +69,7 @@ pub struct ClientAddrConfig {
 /// and requests—even those that are currently waiting.
 #[derive(Debug, Clone, Builder, Deserialize, Eq, PartialEq)]
 #[builder(build_fn(error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 #[serde(deny_unknown_fields)]
 #[non_exhaustive]
 pub struct StreamTimeoutConfig {
@@ -74,17 +77,20 @@ pub struct StreamTimeoutConfig {
     /// to a host?
     #[builder(default = "default_connect_timeout()")]
     #[serde(with = "humantime_serde", default = "default_connect_timeout")]
+    #[builder(attrs(serde(with = "humantime_serde_option")))]
     pub(crate) connect_timeout: Duration,
 
     /// How long should we wait before timing out when resolving a DNS record?
     #[builder(default = "default_dns_resolve_timeout()")]
     #[serde(with = "humantime_serde", default = "default_dns_resolve_timeout")]
+    #[builder(attrs(serde(with = "humantime_serde_option")))]
     pub(crate) resolve_timeout: Duration,
 
     /// How long should we wait before timing out when resolving a DNS
     /// PTR record?
     #[builder(default = "default_dns_resolve_ptr_timeout()")]
     #[serde(with = "humantime_serde", default = "default_dns_resolve_ptr_timeout")]
+    #[builder(attrs(serde(with = "humantime_serde_option")))]
     pub(crate) resolve_ptr_timeout: Duration,
 }
 
@@ -98,14 +104,6 @@ impl Default for ClientAddrConfig {
     }
 }
 
-impl From<ClientAddrConfig> for ClientAddrConfigBuilder {
-    fn from(cfg: ClientAddrConfig) -> ClientAddrConfigBuilder {
-        let mut builder = ClientAddrConfigBuilder::default();
-        builder.allow_local_addrs(cfg.allow_local_addrs);
-        builder
-    }
-}
-
 impl ClientAddrConfig {
     /// Return a new [`ClientAddrConfigBuilder`].
     pub fn builder() -> ClientAddrConfigBuilder {
@@ -120,18 +118,6 @@ impl Default for StreamTimeoutConfig {
     }
 }
 
-impl From<StreamTimeoutConfig> for StreamTimeoutConfigBuilder {
-    fn from(cfg: StreamTimeoutConfig) -> StreamTimeoutConfigBuilder {
-        let mut builder = StreamTimeoutConfigBuilder::default();
-        builder
-            .connect_timeout(cfg.connect_timeout)
-            .resolve_timeout(cfg.resolve_timeout)
-            .resolve_ptr_timeout(cfg.resolve_ptr_timeout);
-
-        builder
-    }
-}
-
 impl StreamTimeoutConfig {
     /// Return a new [`StreamTimeoutConfigBuilder`].
     pub fn builder() -> StreamTimeoutConfigBuilder {
@@ -172,6 +158,7 @@ fn default_dns_resolve_ptr_timeout() -> Duration {
 #[derive(Deserialize, Debug, Clone, Builder, Eq, PartialEq)]
 #[serde(deny_unknown_fields)]
 #[builder(build_fn(error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 pub struct StorageConfig {
     /// Location on disk for cached directory information.
     #[builder(setter(into), default = "default_cache_dir()")]
@@ -225,20 +212,13 @@ impl StorageConfig {
     }
 }
 
-impl From<StorageConfig> for StorageConfigBuilder {
-    fn from(cfg: StorageConfig) -> StorageConfigBuilder {
-        let mut builder = StorageConfigBuilder::default();
-        builder.state_dir(cfg.state_dir).cache_dir(cfg.cache_dir);
-        builder
-    }
-}
-
 /// Configuration for system resources used by Tor.
 ///
 /// You cannot change this section on a running Arti client.
 #[derive(Deserialize, Debug, Clone, Builder, Eq, PartialEq)]
 #[serde(deny_unknown_fields)]
 #[builder(build_fn(error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 #[non_exhaustive]
 pub struct SystemConfig {
     /// Maximum number of file descriptors we should launch with
@@ -265,14 +245,6 @@ impl SystemConfig {
     }
 }
 
-impl From<SystemConfig> for SystemConfigBuilder {
-    fn from(cfg: SystemConfig) -> SystemConfigBuilder {
-        let mut builder = SystemConfigBuilder::default();
-        builder.max_files(cfg.max_files);
-        builder
-    }
-}
-
 /// A configuration used to bootstrap a [`TorClient`](crate::TorClient).
 ///
 /// In order to connect to the Tor network, Arti needs to know a few
@@ -556,36 +528,6 @@ impl TorClientConfigBuilder {
     }
 }
 
-impl From<TorClientConfig> for TorClientConfigBuilder {
-    fn from(cfg: TorClientConfig) -> TorClientConfigBuilder {
-        let TorClientConfig {
-            tor_network,
-            storage,
-            download_schedule,
-            override_net_params,
-            path_rules,
-            preemptive_circuits,
-            circuit_timing,
-            address_filter,
-            stream_timeouts,
-            system,
-        } = cfg;
-
-        TorClientConfigBuilder {
-            tor_network: tor_network.into(),
-            storage: storage.into(),
-            download_schedule: download_schedule.into(),
-            override_net_params,
-            path_rules: path_rules.into(),
-            preemptive_circuits: preemptive_circuits.into(),
-            circuit_timing: circuit_timing.into(),
-            address_filter: address_filter.into(),
-            stream_timeouts: stream_timeouts.into(),
-            system: system.into(),
-        }
-    }
-}
-
 #[cfg(test)]
 mod test {
     #![allow(clippy::unwrap_used)]
@@ -594,7 +536,7 @@ mod test {
     #[test]
     fn defaults() {
         let dflt = TorClientConfig::default();
-        let b2 = TorClientConfigBuilder::from(dflt.clone());
+        let b2 = TorClientConfigBuilder::default();
         let dflt2 = b2.build().unwrap();
         assert_eq!(&dflt, &dflt2);
     }
@@ -640,11 +582,6 @@ mod test {
 
         let val = bld.build().unwrap();
 
-        // Reconstruct, rebuild, and validate.
-        let bld2 = TorClientConfigBuilder::from(val.clone());
-        let val2 = bld2.build().unwrap();
-        assert_eq!(val, val2);
-
         assert_ne!(val, TorClientConfig::default());
     }
 }
diff --git a/crates/arti-config/Cargo.toml b/crates/arti-config/Cargo.toml
index d5e068310680bab68df7af7aca04ffb721a8abec..b1c8115666a10608ee9f2f54b2810f7a2e4328bd 100644
--- a/crates/arti-config/Cargo.toml
+++ b/crates/arti-config/Cargo.toml
@@ -20,7 +20,7 @@ serde = { version = "1.0.103", features = ["derive"] }
 toml = "0.5"
 regex = { version = "1", default-features = false, features = ["std"] }
 thiserror = "1"
-derive_builder = "0.10"
+derive_builder = "0.10.2"
 
 [dev-dependencies]
 tempfile = "3"
diff --git a/crates/arti-config/src/options.rs b/crates/arti-config/src/options.rs
index 4d832f42466b0592c5f47d7bee72f88adb186a6e..d5fbda9dcc815a49e3480a378220a6ea3e7fc9ab 100644
--- a/crates/arti-config/src/options.rs
+++ b/crates/arti-config/src/options.rs
@@ -1,11 +1,8 @@
 //! Handling for arti's configuration formats.
 
 use arti_client::config::{
-    circ,
-    dir::{self, DownloadScheduleConfig, NetworkConfig},
-    ClientAddrConfig, ClientAddrConfigBuilder, StorageConfig, StorageConfigBuilder,
-    StreamTimeoutConfig, StreamTimeoutConfigBuilder, SystemConfig, SystemConfigBuilder,
-    TorClientConfig, TorClientConfigBuilder,
+    circ, dir, ClientAddrConfigBuilder, StorageConfigBuilder, StreamTimeoutConfigBuilder,
+    SystemConfig, SystemConfigBuilder, TorClientConfig, TorClientConfigBuilder,
 };
 use derive_builder::Builder;
 use serde::Deserialize;
@@ -20,6 +17,7 @@ pub(crate) const ARTI_DEFAULTS: &str = concat!(include_str!("./arti_defaults.tom
 #[derive(Deserialize, Debug, Default, Clone, Builder, Eq, PartialEq)]
 #[serde(deny_unknown_fields)]
 #[builder(build_fn(error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 pub struct ApplicationConfig {
     /// If true, we should watch our configuration files for changes, and reload
     /// our configuration when they change.
@@ -33,14 +31,6 @@ pub struct ApplicationConfig {
     watch_configuration: bool,
 }
 
-impl From<ApplicationConfig> for ApplicationConfigBuilder {
-    fn from(cfg: ApplicationConfig) -> Self {
-        let mut builder = ApplicationConfigBuilder::default();
-        builder.watch_configuration(cfg.watch_configuration);
-        builder
-    }
-}
-
 impl ApplicationConfig {
     /// Return true if we're configured to watch for configuration changes.
     pub fn watch_configuration(&self) -> bool {
@@ -53,6 +43,7 @@ impl ApplicationConfig {
 #[serde(deny_unknown_fields)]
 #[non_exhaustive] // TODO(nickm) remove public elements when I revise this.
 #[builder(build_fn(error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 pub struct LoggingConfig {
     /// Filtering directives that determine tracing levels as described at
     /// <https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/targets/struct.Targets.html#impl-FromStr>
@@ -117,19 +108,6 @@ impl LoggingConfig {
     }
 }
 
-impl From<LoggingConfig> for LoggingConfigBuilder {
-    fn from(cfg: LoggingConfig) -> LoggingConfigBuilder {
-        let mut builder = LoggingConfigBuilder::default();
-        if let Some(console) = cfg.console {
-            builder.console(console);
-        }
-        if let Some(journald) = cfg.journald {
-            builder.journald(journald);
-        }
-        builder
-    }
-}
-
 /// Configuration information for an (optionally rotating) logfile.
 #[derive(Deserialize, Debug, Builder, Clone, Eq, PartialEq)]
 pub struct LogfileConfig {
@@ -188,6 +166,7 @@ impl LogfileConfig {
 #[derive(Deserialize, Debug, Clone, Builder, Eq, PartialEq)]
 #[serde(deny_unknown_fields)]
 #[builder(build_fn(error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 pub struct ProxyConfig {
     /// Port to listen on (at localhost) for incoming SOCKS
     /// connections.
@@ -221,14 +200,6 @@ impl ProxyConfig {
     }
 }
 
-impl From<ProxyConfig> for ProxyConfigBuilder {
-    fn from(cfg: ProxyConfig) -> ProxyConfigBuilder {
-        let mut builder = ProxyConfigBuilder::default();
-        builder.socks_port(cfg.socks_port);
-        builder
-    }
-}
-
 /// Structure to hold Arti's configuration options, whether from a
 /// configuration file or the command line.
 //
@@ -243,8 +214,7 @@ impl From<ProxyConfig> for ProxyConfigBuilder {
 ///
 /// NOTE: These are NOT the final options or their final layout. Expect NO
 /// stability here.
-#[derive(Deserialize, Debug, Clone, Eq, PartialEq, Default)]
-#[serde(deny_unknown_fields)]
+#[derive(Debug, Clone, Eq, PartialEq, Default)]
 pub struct ArtiConfig {
     /// Configuration for application behavior.
     application: ApplicationConfig,
@@ -255,51 +225,28 @@ pub struct ArtiConfig {
     /// Logging configuration
     logging: LoggingConfig,
 
-    /// Information about the Tor network we want to connect to.
-    #[serde(default)]
-    tor_network: NetworkConfig,
-
-    /// Directories for storing information on disk
-    storage: StorageConfig,
-
-    /// Information about when and how often to download directory information
-    download_schedule: DownloadScheduleConfig,
-
-    /// Facility to override network parameters from the values set in the
-    /// consensus.
-    #[serde(default)]
-    override_net_params: HashMap<String, i32>,
-
-    /// Information about how to build paths through the network.
-    path_rules: circ::PathConfig,
-
-    /// Information about preemptive circuits
-    preemptive_circuits: circ::PreemptiveCircuitConfig,
-
-    /// Information about how to retry and expire circuits and request for circuits.
-    circuit_timing: circ::CircuitTiming,
-
-    /// Rules about which addresses the client is willing to connect to.
-    address_filter: ClientAddrConfig,
-
-    /// Information about when to time out client requests.
-    stream_timeouts: StreamTimeoutConfig,
-
     /// Information on system resources used by Arti.
     system: SystemConfig,
+
+    /// Configuration of the actual Tor client
+    tor: TorClientConfig,
 }
 
 impl TryFrom<config::Config> for ArtiConfig {
     type Error = config::ConfigError;
     fn try_from(cfg: config::Config) -> Result<ArtiConfig, Self::Error> {
-        cfg.try_deserialize()
+        let builder: ArtiConfigBuilder = cfg.try_deserialize()?;
+        builder
+            .build()
+            .map_err(|e| config::ConfigError::Foreign(Box::new(e)))
     }
 }
 
-impl From<ArtiConfig> for TorClientConfigBuilder {
-    fn from(cfg: ArtiConfig) -> TorClientConfigBuilder {
+// This handwritten impl ought not to exist, but it is needed until #374 is done.
+impl From<ArtiConfigBuilder> for TorClientConfigBuilder {
+    fn from(cfg: ArtiConfigBuilder) -> TorClientConfigBuilder {
         let mut builder = TorClientConfig::builder();
-        let ArtiConfig {
+        let ArtiConfigBuilder {
             storage,
             address_filter,
             path_rules,
@@ -310,14 +257,14 @@ impl From<ArtiConfig> for TorClientConfigBuilder {
             tor_network,
             ..
         } = cfg;
-        *builder.storage() = storage.into();
-        *builder.address_filter() = address_filter.into();
-        *builder.path_rules() = path_rules.into();
-        *builder.preemptive_circuits() = preemptive_circuits.into();
-        *builder.circuit_timing() = circuit_timing.into();
+        *builder.storage() = storage;
+        *builder.address_filter() = address_filter;
+        *builder.path_rules() = path_rules;
+        *builder.preemptive_circuits() = preemptive_circuits;
+        *builder.circuit_timing() = circuit_timing;
         *builder.override_net_params() = override_net_params;
-        *builder.download_schedule() = download_schedule.into();
-        *builder.tor_network() = tor_network.into();
+        *builder.download_schedule() = download_schedule;
+        *builder.tor_network() = tor_network;
         builder
     }
 }
@@ -325,8 +272,7 @@ impl From<ArtiConfig> for TorClientConfigBuilder {
 impl ArtiConfig {
     /// Construct a [`TorClientConfig`] based on this configuration.
     pub fn tor_client_config(&self) -> Result<TorClientConfig, ConfigBuildError> {
-        let builder: TorClientConfigBuilder = self.clone().into();
-        builder.build()
+        Ok(self.tor.clone())
     }
 
     /// Return a new ArtiConfigBuilder.
@@ -356,33 +302,48 @@ impl ArtiConfig {
 ///
 /// Unlike other builder types in Arti, this builder works by exposing an
 /// inner builder for each section in the [`TorClientConfig`].
-#[derive(Default, Clone)]
+#[derive(Default, Clone, Deserialize)]
+// This ought to be replaced by a derive-builder generated struct (probably as part of #374),
+// but currently derive-builder can't do this.
 pub struct ArtiConfigBuilder {
     /// Builder for the application section
+    #[serde(default)]
     application: ApplicationConfigBuilder,
     /// Builder for the proxy section.
+    #[serde(default)]
     proxy: ProxyConfigBuilder,
     /// Builder for the logging section.
+    #[serde(default)]
     logging: LoggingConfigBuilder,
     /// Builder for the storage section.
+    #[serde(default)]
     storage: StorageConfigBuilder,
     /// Builder for the tor_network section.
+    #[serde(default)]
     tor_network: dir::NetworkConfigBuilder,
     /// Builder for the download_schedule section.
+    #[serde(default)]
     download_schedule: dir::DownloadScheduleConfigBuilder,
     /// In-progress object for the override_net_params section.
+    #[serde(default)]
     override_net_params: HashMap<String, i32>,
     /// Builder for the path_rules section.
+    #[serde(default)]
     path_rules: circ::PathConfigBuilder,
     /// Builder for the preemptive_circuits section.
+    #[serde(default)]
     preemptive_circuits: circ::PreemptiveCircuitConfigBuilder,
     /// Builder for the circuit_timing section.
+    #[serde(default)]
     circuit_timing: circ::CircuitTimingBuilder,
     /// Builder for the address_filter section.
+    #[serde(default)]
     address_filter: ClientAddrConfigBuilder,
     /// Builder for the stream timeout rules.
+    #[serde(default)]
     stream_timeouts: StreamTimeoutConfigBuilder,
     /// Builder for system resource configuration.
+    #[serde(default)]
     system: SystemConfigBuilder,
 }
 
@@ -395,51 +356,15 @@ impl ArtiConfigBuilder {
             .map_err(|e| e.within("application"))?;
         let proxy = self.proxy.build().map_err(|e| e.within("proxy"))?;
         let logging = self.logging.build().map_err(|e| e.within("logging"))?;
-        let tor_network = self
-            .tor_network
-            .build()
-            .map_err(|e| e.within("tor_network"))?;
-        let storage = self.storage.build().map_err(|e| e.within("storage"))?;
-        let download_schedule = self
-            .download_schedule
-            .build()
-            .map_err(|e| e.within("download_schedule"))?;
-        let override_net_params = self.override_net_params.clone();
-        let path_rules = self
-            .path_rules
-            .build()
-            .map_err(|e| e.within("path_rules"))?;
-        let preemptive_circuits = self
-            .preemptive_circuits
-            .build()
-            .map_err(|e| e.within("preemptive_circuits"))?;
-        let circuit_timing = self
-            .circuit_timing
-            .build()
-            .map_err(|e| e.within("circuit_timing"))?;
-        let address_filter = self
-            .address_filter
-            .build()
-            .map_err(|e| e.within("address_filter"))?;
-        let stream_timeouts = self
-            .stream_timeouts
-            .build()
-            .map_err(|e| e.within("stream_timeouts"))?;
         let system = self.system.build().map_err(|e| e.within("system"))?;
+        let tor = TorClientConfigBuilder::from(self.clone());
+        let tor = tor.build()?;
         Ok(ArtiConfig {
             application,
             proxy,
             logging,
-            tor_network,
-            storage,
-            download_schedule,
-            override_net_params,
-            path_rules,
-            preemptive_circuits,
-            circuit_timing,
-            address_filter,
-            stream_timeouts,
             system,
+            tor,
         })
     }
 
@@ -554,26 +479,6 @@ impl ArtiConfigBuilder {
     }
 }
 
-impl From<ArtiConfig> for ArtiConfigBuilder {
-    fn from(cfg: ArtiConfig) -> ArtiConfigBuilder {
-        ArtiConfigBuilder {
-            application: cfg.application.into(),
-            proxy: cfg.proxy.into(),
-            logging: cfg.logging.into(),
-            storage: cfg.storage.into(),
-            tor_network: cfg.tor_network.into(),
-            download_schedule: cfg.download_schedule.into(),
-            override_net_params: cfg.override_net_params,
-            path_rules: cfg.path_rules.into(),
-            preemptive_circuits: cfg.preemptive_circuits.into(),
-            circuit_timing: cfg.circuit_timing.into(),
-            address_filter: cfg.address_filter.into(),
-            stream_timeouts: cfg.stream_timeouts.into(),
-            system: cfg.system.into(),
-        }
-    }
-}
-
 #[cfg(test)]
 mod test {
     #![allow(clippy::unwrap_used)]
@@ -598,11 +503,6 @@ mod test {
         let default = ArtiConfig::default();
         assert_eq!(&parsed, &default);
 
-        // Make sure we can round-trip through the builder.
-        let bld: ArtiConfigBuilder = default.into();
-        let rebuilt = bld.build().unwrap();
-        assert_eq!(&rebuilt, &parsed);
-
         // Make sure that the client configuration this gives us is the default one.
         let client_config = parsed.tor_client_config().unwrap();
         let dflt_client_config = TorClientConfig::default();
@@ -658,11 +558,6 @@ mod test {
 
         let val = bld.build().unwrap();
 
-        // Reconstruct, rebuild, and validate.
-        let bld2 = ArtiConfigBuilder::from(val.clone());
-        let val2 = bld2.build().unwrap();
-        assert_eq!(val, val2);
-
         assert_ne!(val, ArtiConfig::default());
     }
 }
diff --git a/crates/tor-basic-utils/Cargo.toml b/crates/tor-basic-utils/Cargo.toml
index e9ec806110616c5905f98c694e7d6806081b734e..ce445b123a9fced9d9a482907943dc3a31193134 100644
--- a/crates/tor-basic-utils/Cargo.toml
+++ b/crates/tor-basic-utils/Cargo.toml
@@ -11,6 +11,12 @@ categories = ["rust-patterns"] # We must put *something* here and this will do
 repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
 
 [dependencies]
+serde = { version = "1.0.103", features = ["derive"], optional = true }
+humantime-serde-crate = { package = "humantime-serde", version = "1", optional = true }
+
+[features]
+default = ["humantime-serde"]
+humantime-serde = ["humantime-serde-crate", "serde"]
 
 [dev-dependencies]
 educe = "0.4.6"
diff --git a/crates/tor-basic-utils/src/humantime_serde_option.rs b/crates/tor-basic-utils/src/humantime_serde_option.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c2f4ef8f3855ad01e2cce7a7e4f45964e70e2171
--- /dev/null
+++ b/crates/tor-basic-utils/src/humantime_serde_option.rs
@@ -0,0 +1,24 @@
+//! Module to adaopt `humantime_serde` to `Option<Duration>`
+
+use humantime_serde_crate::Serde as HtSerde;
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+/// Serializes an `Option<Duration>` or `Option<SystemTime>` via the humantime crate.
+pub fn serialize<T, S>(d: &Option<T>, s: S) -> Result<S::Ok, S::Error>
+where
+    for<'a> HtSerde<&'a T>: Serialize,
+    S: Serializer,
+{
+    let nested: Option<HtSerde<&T>> = d.as_ref().map(Into::into);
+    nested.serialize(s)
+}
+
+/// Deserialize an `Option<Duration>` or `Option<SystemTime>` via the humantime crate.
+pub fn deserialize<'a, T, D>(d: D) -> Result<Option<T>, D::Error>
+where
+    HtSerde<T>: Deserialize<'a>,
+    D: Deserializer<'a>,
+{
+    let got: Option<HtSerde<T>> = Deserialize::deserialize(d)?;
+    Ok(got.map(HtSerde::into_inner))
+}
diff --git a/crates/tor-basic-utils/src/lib.rs b/crates/tor-basic-utils/src/lib.rs
index 811cb686690c1181c1655e81a732212a1e5f8e09..fc09b62108383b717ee7a46bdaef59ead311b06d 100644
--- a/crates/tor-basic-utils/src/lib.rs
+++ b/crates/tor-basic-utils/src/lib.rs
@@ -41,6 +41,9 @@
 
 use std::fmt;
 
+#[cfg(feature = "humantime-serde")]
+pub mod humantime_serde_option;
+
 // ----------------------------------------------------------------------
 
 /// Function with the signature of `Debug::fmt` that just prints `".."`
diff --git a/crates/tor-circmgr/Cargo.toml b/crates/tor-circmgr/Cargo.toml
index 660fd2ea8ba3e234a822630e47867a08766e066c..5c624f6483b64e1f36afbe7f789e6751d367adf5 100644
--- a/crates/tor-circmgr/Cargo.toml
+++ b/crates/tor-circmgr/Cargo.toml
@@ -33,7 +33,7 @@ tor-rtcompat = { path="../tor-rtcompat", version = "0.1.0"}
 
 async-trait = "0.1.2"
 bounded-vec-deque = "0.1"
-derive_builder = "0.10"
+derive_builder = "0.10.2"
 educe = "0.4.6"
 futures = "0.3.14"
 humantime-serde = "1"
diff --git a/crates/tor-circmgr/src/config.rs b/crates/tor-circmgr/src/config.rs
index def852c61365bd50fd598f40e8426376339d251d..a866151d875bc98c61ddc2ee93fb1aeb0dc671db 100644
--- a/crates/tor-circmgr/src/config.rs
+++ b/crates/tor-circmgr/src/config.rs
@@ -4,6 +4,7 @@
 //!
 //! Most types in this module are re-exported by `arti-client`.
 
+use tor_basic_utils::humantime_serde_option;
 use tor_config::ConfigBuildError;
 
 use derive_builder::Builder;
@@ -22,6 +23,7 @@ use std::time::Duration;
 /// restrictive.
 #[derive(Debug, Clone, Builder, Deserialize, Eq, PartialEq)]
 #[builder(build_fn(error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 #[serde(deny_unknown_fields)]
 pub struct PathConfig {
     /// Set the length of a bit-prefix for a default IPv4 subnet-family.
@@ -81,16 +83,6 @@ impl Default for PathConfig {
     }
 }
 
-impl From<PathConfig> for PathConfigBuilder {
-    fn from(cfg: PathConfig) -> PathConfigBuilder {
-        let mut builder = PathConfigBuilder::default();
-        builder
-            .ipv4_subnet_family_prefix(cfg.ipv4_subnet_family_prefix)
-            .ipv6_subnet_family_prefix(cfg.ipv6_subnet_family_prefix);
-        builder
-    }
-}
-
 /// Configuration for preemptive circuits.
 ///
 /// Preemptive circuits are built ahead of time, to anticipate client need. This
@@ -103,6 +95,7 @@ impl From<PathConfig> for PathConfigBuilder {
 /// Except as noted, this configuration can be changed on a running Arti client.
 #[derive(Debug, Clone, Builder, Deserialize, Eq, PartialEq)]
 #[builder(build_fn(error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 #[serde(deny_unknown_fields)]
 pub struct PreemptiveCircuitConfig {
     /// If we have at least this many available circuits, we suspend
@@ -128,6 +121,7 @@ pub struct PreemptiveCircuitConfig {
     /// available for that port?
     #[builder(default = "default_preemptive_duration()")]
     #[serde(with = "humantime_serde", default = "default_preemptive_duration")]
+    #[builder(attrs(serde(with = "humantime_serde_option")))]
     pub(crate) prediction_lifetime: Duration,
 
     /// How many available circuits should we try to have, at minimum, for each
@@ -149,12 +143,14 @@ pub struct PreemptiveCircuitConfig {
 /// [#263](https://gitlab.torproject.org/tpo/core/arti/-/issues/263).
 #[derive(Debug, Clone, Builder, Deserialize, Eq, PartialEq)]
 #[builder(build_fn(error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 #[serde(deny_unknown_fields)]
 pub struct CircuitTiming {
     /// How long after a circuit has first been used should we give
     /// it out for new requests?
     #[builder(default = "default_max_dirtiness()")]
     #[serde(with = "humantime_serde", default = "default_max_dirtiness")]
+    #[builder(attrs(serde(with = "humantime_serde_option")))]
     pub(crate) max_dirtiness: Duration,
 
     /// When a circuit is requested, we stop retrying new circuits
@@ -162,6 +158,7 @@ pub struct CircuitTiming {
     // TODO: Impose a maximum or minimum?
     #[builder(default = "default_request_timeout()")]
     #[serde(with = "humantime_serde", default = "default_request_timeout")]
+    #[builder(attrs(serde(with = "humantime_serde_option")))]
     pub(crate) request_timeout: Duration,
 
     /// When a circuit is requested, we stop retrying new circuits after
@@ -176,6 +173,7 @@ pub struct CircuitTiming {
     /// request.
     #[builder(default = "default_request_loyalty()")]
     #[serde(with = "humantime_serde", default = "default_request_loyalty")]
+    #[builder(attrs(serde(with = "humantime_serde_option")))]
     pub(crate) request_loyalty: Duration,
 }
 
@@ -236,18 +234,6 @@ impl CircuitTiming {
     }
 }
 
-impl From<CircuitTiming> for CircuitTimingBuilder {
-    fn from(cfg: CircuitTiming) -> CircuitTimingBuilder {
-        let mut builder = CircuitTimingBuilder::default();
-        builder
-            .max_dirtiness(cfg.max_dirtiness)
-            .request_timeout(cfg.request_timeout)
-            .request_max_retries(cfg.request_max_retries)
-            .request_loyalty(cfg.request_loyalty);
-        builder
-    }
-}
-
 impl Default for PreemptiveCircuitConfig {
     fn default() -> Self {
         PreemptiveCircuitConfigBuilder::default()
@@ -263,18 +249,6 @@ impl PreemptiveCircuitConfig {
     }
 }
 
-impl From<PreemptiveCircuitConfig> for PreemptiveCircuitConfigBuilder {
-    fn from(cfg: PreemptiveCircuitConfig) -> PreemptiveCircuitConfigBuilder {
-        let mut builder = PreemptiveCircuitConfigBuilder::default();
-        builder
-            .disable_at_threshold(cfg.disable_at_threshold)
-            .initial_predicted_ports(cfg.initial_predicted_ports)
-            .prediction_lifetime(cfg.prediction_lifetime)
-            .min_exit_circs_for_port(cfg.min_exit_circs_for_port);
-        builder
-    }
-}
-
 /// Configuration for a circuit manager.
 ///
 /// This configuration includes information about how to build paths
diff --git a/crates/tor-config/Cargo.toml b/crates/tor-config/Cargo.toml
index 78eb20d2fe567a6ef2d79fd95a64956c294a7906..de3835fa1d3ec0a1f31542d39de2fdc3334cc4b1 100644
--- a/crates/tor-config/Cargo.toml
+++ b/crates/tor-config/Cargo.toml
@@ -18,7 +18,7 @@ expand-paths = ["shellexpand", "directories"]
 tor-error = { path="../tor-error", version = "0.1.0"}
 
 thiserror = "1"
-derive_builder = "0.10"
+derive_builder = "0.10.2"
 once_cell = "1"
 serde = { version = "1.0.103", features = ["derive"] }
 shellexpand = { version = "2.1", package = "shellexpand-fork", optional = true }
diff --git a/crates/tor-dirmgr/Cargo.toml b/crates/tor-dirmgr/Cargo.toml
index 70323b0fb85d2754b4a3e4c5c098e406b2c8adb4..c565c7dca258aff02aa9d376c5293090d9817410 100644
--- a/crates/tor-dirmgr/Cargo.toml
+++ b/crates/tor-dirmgr/Cargo.toml
@@ -33,7 +33,7 @@ tor-rtcompat = { path = "../tor-rtcompat", version = "0.1.0"}
 
 async-trait = "0.1.2"
 base64 = "0.13.0"
-derive_builder = "0.10"
+derive_builder = "0.10.2"
 derive_more = "0.99"
 digest = "0.10.0"
 educe = "0.4.6"
diff --git a/crates/tor-dirmgr/src/authority.rs b/crates/tor-dirmgr/src/authority.rs
index 475bce84cef43e654a5d1374429d626c6c281946..175a1e9ad8699af418c8f1aeecfaacd6d80de325 100644
--- a/crates/tor-dirmgr/src/authority.rs
+++ b/crates/tor-dirmgr/src/authority.rs
@@ -14,6 +14,7 @@ use tor_netdoc::doc::authcert::{AuthCert, AuthCertKeyIds};
 // we want our authorities format to be future-proof against adding new info
 // about each authority.
 #[derive(Deserialize, Debug, Clone, Builder, Eq, PartialEq)]
+#[builder(derive(Deserialize))]
 pub struct Authority {
     /// A memorable nickname for this authority.
     #[builder(setter(into))]
diff --git a/crates/tor-dirmgr/src/config.rs b/crates/tor-dirmgr/src/config.rs
index 536d6bf4d82a8aa02cf1dd92446f4b72374c0c67..43db89af7d8fb8a88fb0407c39ea85062ecdd73c 100644
--- a/crates/tor-dirmgr/src/config.rs
+++ b/crates/tor-dirmgr/src/config.rs
@@ -28,6 +28,7 @@ use serde::Deserialize;
 #[derive(Deserialize, Debug, Clone, Builder, Eq, PartialEq)]
 #[serde(deny_unknown_fields)]
 #[builder(build_fn(validate = "Self::validate", error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 pub struct NetworkConfig {
     /// List of locations to look in when downloading directory information, if
     /// we don't actually have a directory yet.
@@ -62,16 +63,6 @@ impl Default for NetworkConfig {
     }
 }
 
-impl From<NetworkConfig> for NetworkConfigBuilder {
-    fn from(cfg: NetworkConfig) -> NetworkConfigBuilder {
-        let mut builder = NetworkConfigBuilder::default();
-        builder
-            .fallback_caches(cfg.fallback_caches)
-            .authorities(cfg.authorities);
-        builder
-    }
-}
-
 impl NetworkConfig {
     /// Return a new builder to construct a NetworkConfig.
     pub fn builder() -> NetworkConfigBuilder {
@@ -110,6 +101,7 @@ impl NetworkConfigBuilder {
 #[derive(Deserialize, Debug, Clone, Builder, Eq, PartialEq)]
 #[serde(deny_unknown_fields)]
 #[builder(build_fn(error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 pub struct DownloadScheduleConfig {
     /// Top-level configuration for how to retry our initial bootstrap attempt.
     #[serde(default = "default_retry_bootstrap")]
@@ -157,18 +149,6 @@ impl DownloadScheduleConfig {
     }
 }
 
-impl From<DownloadScheduleConfig> for DownloadScheduleConfigBuilder {
-    fn from(cfg: DownloadScheduleConfig) -> DownloadScheduleConfigBuilder {
-        let mut builder = DownloadScheduleConfigBuilder::default();
-        builder
-            .retry_bootstrap(cfg.retry_bootstrap)
-            .retry_consensus(cfg.retry_consensus)
-            .retry_certs(cfg.retry_certs)
-            .retry_microdescs(cfg.retry_microdescs);
-        builder
-    }
-}
-
 /// Configuration type for network directory operations.
 ///
 /// This type is immutable once constructed.
@@ -181,6 +161,7 @@ impl From<DownloadScheduleConfig> for DownloadScheduleConfigBuilder {
 /// running Arti client. Those that cannot are documented.
 #[derive(Debug, Clone, Builder, Eq, PartialEq)]
 #[builder(build_fn(error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 pub struct DirMgrConfig {
     /// Location to use for storing and reading current-format
     /// directory information.
diff --git a/crates/tor-guardmgr/Cargo.toml b/crates/tor-guardmgr/Cargo.toml
index ea1b4c345a4a1ec0830ac05fe8b9af0e8cd8f6b5..55dfedec702331e7fb8b9738645b4f07ee58cf5d 100644
--- a/crates/tor-guardmgr/Cargo.toml
+++ b/crates/tor-guardmgr/Cargo.toml
@@ -29,7 +29,7 @@ tor-proto = {  path="../tor-proto", version = "0.1.0"}
 tor-rtcompat = { path="../tor-rtcompat", version = "0.1.0"}
 tor-units = { path="../tor-units", version = "0.1.0"}
 
-derive_builder = "0.10"
+derive_builder = "0.10.2"
 educe = "0.4.6"
 futures = "0.3.14"
 humantime-serde = "1"
diff --git a/crates/tor-netdir/Cargo.toml b/crates/tor-netdir/Cargo.toml
index e1c23baaf711340c2fb0fd97e04a22a1d418c8fc..61d846199d5b1b78184e5e44cc142ca56ff00cd1 100644
--- a/crates/tor-netdir/Cargo.toml
+++ b/crates/tor-netdir/Cargo.toml
@@ -33,7 +33,7 @@ tor-protover = { path="../tor-protover", version = "0.1.0"}
 tor-units = { path="../tor-units", version = "0.1.0"}
 
 bitflags = "1"
-derive_builder = "0.10"
+derive_builder = "0.10.2"
 derive_more = "0.99"
 hex = { version = "0.4", optional = true }
 hex-literal = { version = "0.3", optional = true }
diff --git a/crates/tor-netdir/src/fallback.rs b/crates/tor-netdir/src/fallback.rs
index 3de0d9747fd7e74610020e914768bedc5e127682..f4332b9f5483b21dbe43c0ec92ae9669cd854517 100644
--- a/crates/tor-netdir/src/fallback.rs
+++ b/crates/tor-netdir/src/fallback.rs
@@ -27,6 +27,7 @@ use std::net::SocketAddr;
 // be future-proof against adding new info about each fallback.
 #[derive(Debug, Clone, Deserialize, Builder, Eq, PartialEq)]
 #[builder(build_fn(validate = "FallbackDirBuilder::validate", error = "ConfigBuildError"))]
+#[builder(derive(Deserialize))]
 pub struct FallbackDir {
     /// RSA identity for the directory relay
     rsa_identity: RsaIdentity,
diff --git a/doc/semver_status.md b/doc/semver_status.md
index 26568c4419e9be6eae467a43d2b0b5b32fe05974..ad348cf2682c31d84af92f83f766432352389df1 100644
--- a/doc/semver_status.md
+++ b/doc/semver_status.md
@@ -19,6 +19,13 @@ We can delete older sections here after we bump the releases.
 
 ## Since Arti 0.1.0
 
+arti-client, arti-config, tor-circmgr, tor-dirmgr:
+
+  Drop conversion from FooConfig to FooConfigBuilder for many Foo.
+  Further change in this area is expected.
+
+  Drop impl Deserialize for ArtiConfig.
+
 tor-llcrypto:
 
   new-api: Added RsaIdentity::from_hex().