diff --git a/Cargo.lock b/Cargo.lock
index 5ca030d7..46d4abc7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -858,10 +858,8 @@ dependencies = [
  "arbitrary",
  "crc32fast",
  "data-encoding",
- "hex",
  "impls",
  "serde",
- "serde_bytes",
  "serde_cbor",
  "serde_json",
  "serde_test",
diff --git a/rust/ic_principal/Cargo.toml b/rust/ic_principal/Cargo.toml
index 1ccc0093..9463af35 100644
--- a/rust/ic_principal/Cargo.toml
+++ b/rust/ic_principal/Cargo.toml
@@ -6,40 +6,48 @@ edition = "2021"
 description = "Principal type used on the Internet Computer."
 homepage = "https://internetcomputer.org/docs/current/references/id-encoding-spec"
 documentation = "https://docs.rs/ic_principal"
-repository="https://github.com/dfinity/candid"
+repository = "https://github.com/dfinity/candid"
 license = "Apache-2.0"
 readme = "README.md"
 categories = ["data-structures", "no-std"]
 keywords = ["internet-computer", "types", "dfinity"]
 include = ["src", "Cargo.toml", "LICENSE", "README.md"]
 
-[dependencies]
-crc32fast = "1.3.0"
-data-encoding = "2.3.2"
-hex = "0.4.3"
-sha2 = "0.10.1"
-thiserror = "1.0.30"
-
 [dev-dependencies]
 serde_cbor = "0.11.2"
 serde_json = "1.0.74"
 serde_test = "1.0.137"
 impls = "1"
 
+[dependencies.arbitrary]
+workspace = true
+optional = true
+
+[dependencies.crc32fast]
+version = "1.3.0"
+optional = true
+
+[dependencies.data-encoding]
+version = "2.3.2"
+optional = true
+
 [dependencies.serde]
 version = "1.0.115"
 features = ["derive"]
 optional = true
 
-[dependencies.serde_bytes]
-version = "0.11.5"
+[dependencies.sha2]
+version = "0.10.1"
 optional = true
 
-[dependencies.arbitrary]
-workspace = true
+[dependencies.thiserror]
+version = "1.0.30"
 optional = true
 
 [features]
-# Default features include serde support.
-default = ['serde', 'serde_bytes']
-arbitrary = ['default', 'dep:arbitrary']
+all = ['arbitrary', 'default']
+default = ['convert', 'self_authenticating', 'serde']
+arbitrary = ['dep:arbitrary', 'serde']
+convert = ['dep:crc32fast', 'dep:data-encoding', 'dep:thiserror']
+self_authenticating = ['dep:sha2']
+serde = ['dep:serde', 'convert']
diff --git a/rust/ic_principal/src/lib.rs b/rust/ic_principal/src/lib.rs
index 9860e8b9..fcf7ea7e 100644
--- a/rust/ic_principal/src/lib.rs
+++ b/rust/ic_principal/src/lib.rs
@@ -1,15 +1,19 @@
+#[cfg(feature = "arbitrary")]
+use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured};
 #[cfg(feature = "serde")]
 use serde::{Deserialize, Serialize};
+#[cfg(feature = "self_authenticating")]
 use sha2::{Digest, Sha224};
+#[cfg(feature = "convert")]
 use std::convert::TryFrom;
+#[cfg(feature = "convert")]
 use std::fmt::Write;
+#[cfg(feature = "convert")]
 use thiserror::Error;
 
-#[cfg(feature = "arbitrary")]
-use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured};
-
 /// An error happened while encoding, decoding or serializing a [`Principal`].
 #[derive(Error, Clone, Debug, Eq, PartialEq)]
+#[cfg(feature = "convert")]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 pub enum PrincipalError {
     #[error("Bytes is longer than 29 bytes.")]
@@ -49,12 +53,15 @@ pub enum PrincipalError {
 ///
 /// Example of using a Principal object:
 /// ```
+/// # #[cfg(feature = "convert")] {
 /// use ic_principal::Principal;
 ///
 /// let text = "aaaaa-aa";  // The management canister ID.
 /// let principal = Principal::from_text(text).expect("Could not decode the principal.");
 /// assert_eq!(principal.as_slice(), &[]);
 /// assert_eq!(principal.to_text(), text);
+///
+/// # }
 /// ```
 ///
 /// Serialization is enabled with the "serde" feature. It supports serializing
@@ -62,6 +69,7 @@ pub enum PrincipalError {
 /// readable serializers.
 ///
 /// ```
+/// # #[cfg(all(feature = "convert", feature = "serde"))] {
 /// use ic_principal::Principal;
 /// use serde::{Deserialize, Serialize};
 /// use std::str::FromStr;
@@ -85,6 +93,7 @@ pub enum PrincipalError {
 ///     serde_cbor::to_vec(&Data { id: id.clone() }).unwrap(),
 ///     &[161, 98, 105, 100, 73, 239, 205, 171, 0, 0, 0, 0, 0, 1],
 /// );
+/// # }
 /// ```
 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub struct Principal {
@@ -98,8 +107,10 @@ pub struct Principal {
 
 impl Principal {
     pub const MAX_LENGTH_IN_BYTES: usize = 29;
+    #[allow(dead_code)]
     const CRC_LENGTH_IN_BYTES: usize = 4;
 
+    #[allow(dead_code)]
     const SELF_AUTHENTICATING_TAG: u8 = 2;
     const ANONYMOUS_TAG: u8 = 4;
 
@@ -112,6 +123,7 @@ impl Principal {
     }
 
     /// Construct a self-authenticating ID from public key
+    #[cfg(feature = "self_authenticating")]
     pub fn self_authenticating<P: AsRef<[u8]>>(public_key: P) -> Self {
         let public_key = public_key.as_ref();
         let hash = Sha224::digest(public_key);
@@ -132,39 +144,48 @@ impl Principal {
         Self { len: 1, bytes }
     }
 
+    /// Returns `None` if the slice exceeds the max length.
+    const fn from_slice_core(slice: &[u8]) -> Option<Self> {
+        match slice.len() {
+            len @ 0..=Self::MAX_LENGTH_IN_BYTES => {
+                let mut bytes = [0; Self::MAX_LENGTH_IN_BYTES];
+                let mut i = 0;
+                while i < len {
+                    bytes[i] = slice[i];
+                    i += 1;
+                }
+                Some(Self {
+                    len: len as u8,
+                    bytes,
+                })
+            }
+            _ => None,
+        }
+    }
+
     /// Construct a [`Principal`] from a slice of bytes.
     ///
     /// # Panics
     ///
     /// Panics if the slice is longer than 29 bytes.
     pub const fn from_slice(slice: &[u8]) -> Self {
-        match Self::try_from_slice(slice) {
-            Ok(v) => v,
+        match Self::from_slice_core(slice) {
+            Some(principal) => principal,
             _ => panic!("slice length exceeds capacity"),
         }
     }
 
     /// Construct a [`Principal`] from a slice of bytes.
+    #[cfg(feature = "convert")]
     pub const fn try_from_slice(slice: &[u8]) -> Result<Self, PrincipalError> {
-        const MAX_LENGTH_IN_BYTES: usize = Principal::MAX_LENGTH_IN_BYTES;
-        match slice.len() {
-            len @ 0..=MAX_LENGTH_IN_BYTES => {
-                let mut bytes = [0; MAX_LENGTH_IN_BYTES];
-                let mut i = 0;
-                while i < len {
-                    bytes[i] = slice[i];
-                    i += 1;
-                }
-                Ok(Self {
-                    len: len as u8,
-                    bytes,
-                })
-            }
-            _ => Err(PrincipalError::BytesTooLong()),
+        match Self::from_slice_core(slice) {
+            Some(principal) => Ok(principal),
+            None => Err(PrincipalError::BytesTooLong()),
         }
     }
 
     /// Parse a [`Principal`] from text representation.
+    #[cfg(feature = "convert")]
     pub fn from_text<S: AsRef<str>>(text: S) -> Result<Self, PrincipalError> {
         // Strategy: Parse very liberally, then pretty-print and compare output
         // This is both simpler and yields better error messages
@@ -206,6 +227,7 @@ impl Principal {
     }
 
     /// Convert [`Principal`] to text representation.
+    #[cfg(feature = "convert")]
     pub fn to_text(&self) -> String {
         format!("{self}")
     }
@@ -217,6 +239,7 @@ impl Principal {
     }
 }
 
+#[cfg(feature = "convert")]
 impl std::fmt::Display for Principal {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         let blob: &[u8] = self.as_slice();
@@ -244,6 +267,7 @@ impl std::fmt::Display for Principal {
     }
 }
 
+#[cfg(feature = "convert")]
 impl std::str::FromStr for Principal {
     type Err = PrincipalError;
 
@@ -252,6 +276,7 @@ impl std::str::FromStr for Principal {
     }
 }
 
+#[cfg(feature = "convert")]
 impl TryFrom<&str> for Principal {
     type Error = PrincipalError;
 
@@ -260,6 +285,7 @@ impl TryFrom<&str> for Principal {
     }
 }
 
+#[cfg(feature = "convert")]
 impl TryFrom<Vec<u8>> for Principal {
     type Error = PrincipalError;
 
@@ -268,6 +294,7 @@ impl TryFrom<Vec<u8>> for Principal {
     }
 }
 
+#[cfg(feature = "convert")]
 impl TryFrom<&Vec<u8>> for Principal {
     type Error = PrincipalError;
 
@@ -276,6 +303,7 @@ impl TryFrom<&Vec<u8>> for Principal {
     }
 }
 
+#[cfg(feature = "convert")]
 impl TryFrom<&[u8]> for Principal {
     type Error = PrincipalError;
 
diff --git a/rust/ic_principal/tests/principal.rs b/rust/ic_principal/tests/principal.rs
index 25393889..541e719e 100644
--- a/rust/ic_principal/tests/principal.rs
+++ b/rust/ic_principal/tests/principal.rs
@@ -1,18 +1,23 @@
 // #![allow(deprecated)]
 
 use ic_principal::Principal;
+#[cfg(feature = "convert")]
 use ic_principal::PrincipalError;
 
 const MANAGEMENT_CANISTER_BYTES: [u8; 0] = [];
+#[allow(dead_code)]
 const MANAGEMENT_CANISTER_TEXT: &str = "aaaaa-aa";
 
 const ANONYMOUS_CANISTER_BYTES: [u8; 1] = [4u8];
+#[allow(dead_code)]
 const ANONYMOUS_CANISTER_TEXT: &str = "2vxsx-fae";
 
 const TEST_CASE_BYTES: [u8; 9] = [0xef, 0xcd, 0xab, 0, 0, 0, 0, 0, 1];
+#[allow(dead_code)]
 const TEST_CASE_TEXT: &str = "2chl6-4hpzw-vqaaa-aaaaa-c";
 
-mod convert_from_bytes {
+#[cfg(feature = "convert")]
+mod try_convert_from_bytes {
     use super::*;
 
     #[test]
@@ -35,6 +40,10 @@ mod convert_from_bytes {
             Err(PrincipalError::BytesTooLong())
         );
     }
+}
+
+mod convert_from_bytes {
+    use super::*;
 
     #[test]
     fn from_test_case_ok() {
@@ -56,6 +65,7 @@ mod convert_from_bytes {
     }
 }
 
+#[cfg(feature = "convert")]
 mod convert_from_text {
     use super::*;
 
@@ -173,6 +183,7 @@ mod convert_to_bytes {
     }
 
     #[test]
+    #[cfg(feature = "convert")]
     fn test_case_to_bytes_correct() {
         assert_eq!(
             Principal::from_text(TEST_CASE_TEXT).unwrap().as_slice(),
@@ -181,6 +192,7 @@ mod convert_to_bytes {
     }
 }
 
+#[cfg(feature = "convert")]
 mod convert_to_text {
     use super::*;
 
@@ -211,6 +223,7 @@ mod convert_to_text {
     }
 }
 
+#[cfg(feature = "serde")]
 mod ser_de {
     use super::*;
     use serde_test::{assert_tokens, Configure, Token};
@@ -232,24 +245,32 @@ mod ser_de {
 
 #[test]
 fn impl_traits() {
+    #[cfg(feature = "serde")]
     use serde::{Deserialize, Serialize};
+    #[cfg(feature = "convert")]
     use std::convert::TryFrom;
-    use std::fmt::{Debug, Display};
+    use std::fmt::Debug;
+    #[cfg(feature = "convert")]
+    use std::fmt::Display;
     use std::hash::Hash;
+    #[cfg(feature = "convert")]
     use std::str::FromStr;
 
     assert!(impls::impls!(
-        Principal: Debug & Display & Clone & Copy & Eq & PartialOrd & Ord & Hash
+        Principal: Debug & Clone & Copy & Eq & PartialOrd & Ord & Hash
     ));
 
+    #[cfg(feature = "convert")]
     assert!(
-        impls::impls!(Principal: FromStr & TryFrom<&'static str> & TryFrom<Vec<u8>> & TryFrom<&'static Vec<u8>> & TryFrom<&'static [u8]> & AsRef<[u8]>)
+        impls::impls!(Principal: Display & FromStr & TryFrom<&'static str> & TryFrom<Vec<u8>> & TryFrom<&'static Vec<u8>> & TryFrom<&'static [u8]> & AsRef<[u8]>)
     );
 
+    #[cfg(feature = "serde")]
     assert!(impls::impls!(Principal: Serialize & Deserialize<'static>));
 }
 
 #[test]
+#[cfg(feature = "convert")]
 fn long_blobs_ending_04_is_valid_principal() {
     let blob: [u8; 18] = [
         10, 116, 105, 100, 0, 0, 0, 0, 0, 144, 0, 51, 1, 1, 0, 0, 0, 4,
@@ -258,6 +279,7 @@ fn long_blobs_ending_04_is_valid_principal() {
 }
 
 #[test]
+#[cfg(feature = "self_authenticating")]
 fn self_authenticating_ok() {
     // self_authenticating doesn't verify the input bytes
     // this test checks:
@@ -265,10 +287,9 @@ fn self_authenticating_ok() {
     // 2. 0x02 was added in the end
     // 3. total length is 29
     let p1 = Principal::self_authenticating([]);
-    let p2 = Principal::try_from_slice(&[
+    let p2 = Principal::from_slice(&[
         209, 74, 2, 140, 42, 58, 43, 201, 71, 97, 2, 187, 40, 130, 52, 196, 21, 162, 176, 31, 130,
         142, 166, 42, 197, 179, 228, 47, 2,
-    ])
-    .unwrap();
+    ]);
     assert_eq!(p1, p2);
 }