Repository: toml-rs/toml
Pull request #878 (2025-04-28 19:26:16)

fix(serde): Skip fields with newtypes wrapping None 
I think this change is justifiable because it makes the `Option<T>` and
`NewtypeOption<T>` cases have the same analogeous behavior as
`serde_json`
- `serde_json` serializes both to `null`
- `toml` serializes both to a non-existent field
- `serde_json` and `toml` require `#[serde(default)]` for deserialization of `NewtypeOption<T>`

The rest is part of cleanup in prep for `toml` to call `toml_write`


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 32c7b07c..c106729c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -894,9 +894,9 @@ dependencies = [
 
 [[package]]
 name = "toml-test"
-version = "2.0.1"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a342200769d5323a4c2f3415f74d4489af34e465fc2a77bbafae9784c4f15e56"
+checksum = "c6155c3b2ba169c1c2a7b33f3384db6af7a53ecf332bb35f36d5b94712d7e4bf"
 dependencies = [
  "chrono",
  "ryu",
@@ -906,18 +906,18 @@ dependencies = [
 
 [[package]]
 name = "toml-test-data"
-version = "2.3.1"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a73cc38bcc02366c009331876cb0857f8941942ef080f0f2c61de0de620c490"
+checksum = "a13877b78b58f6bba562aaca6930fd266b9e8c569aef815352552b302d0b8620"
 dependencies = [
  "include_dir",
 ]
 
 [[package]]
 name = "toml-test-harness"
-version = "1.3.1"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8dd3d6b7c7e954a1d2084f41dc8a070ef9578af8714645744311d0fd7342b82"
+checksum = "c263faeb344cef69a467a8a2b4d4dd3c729efd9379b5383158f969c3da50c057"
 dependencies = [
  "ignore",
  "libtest-mimic",
diff --git a/crates/toml_edit/src/ser/array.rs b/crates/toml_edit/src/ser/array.rs
index a42cc661..2b4dc943 100644
--- a/crates/toml_edit/src/ser/array.rs
+++ b/crates/toml_edit/src/ser/array.rs
@@ -82,3 +82,39 @@ impl serde::ser::SerializeTupleStruct for SerializeValueArray {
         serde::ser::SerializeSeq::end(self)
     }
 }
+
+pub struct SerializeTupleVariant {
+    variant: &'static str,
+    inner: SerializeValueArray,
+}
+
+impl SerializeTupleVariant {
+    pub(crate) fn tuple(variant: &'static str, len: usize) -> Self {
+        Self {
+            variant,
+            inner: SerializeValueArray::with_capacity(len),
+        }
+    }
+}
+
+impl serde::ser::SerializeTupleVariant for SerializeTupleVariant {
+    type Ok = crate::Value;
+    type Error = Error;
+
+    fn serialize_field<T>(&mut self, value: &T) -> Result<(), Error>
+    where
+        T: serde::ser::Serialize + ?Sized,
+    {
+        serde::ser::SerializeSeq::serialize_element(&mut self.inner, value)
+    }
+
+    fn end(self) -> Result<Self::Ok, Self::Error> {
+        let inner = serde::ser::SerializeSeq::end(self.inner)?;
+        let mut items = crate::table::KeyValuePairs::new();
+        let value = crate::Item::Value(inner);
+        items.insert(crate::Key::new(self.variant), value);
+        Ok(crate::Value::InlineTable(crate::InlineTable::with_pairs(
+            items,
+        )))
+    }
+}
diff --git a/crates/toml_edit/src/ser/map.rs b/crates/toml_edit/src/ser/map.rs
index c0ded871..3a4c037c 100644
--- a/crates/toml_edit/src/ser/map.rs
+++ b/crates/toml_edit/src/ser/map.rs
@@ -1,3 +1,4 @@
+use super::array::SerializeTupleVariant;
 use super::array::SerializeValueArray;
 use super::key::KeySerializer;
 use super::value::ValueSerializer;
@@ -169,8 +170,9 @@ impl serde::ser::SerializeMap for SerializeInlineTable {
     where
         T: serde::ser::Serialize + ?Sized,
     {
-        let mut value_serializer = MapValueSerializer::new();
-        let res = value.serialize(&mut value_serializer);
+        let mut is_none = false;
+        let value_serializer = MapValueSerializer::new(&mut is_none);
+        let res = value.serialize(value_serializer);
         match res {
             Ok(item) => {
                 let key = self.key.take().unwrap();
@@ -178,7 +180,7 @@ impl serde::ser::SerializeMap for SerializeInlineTable {
                 self.items.insert(key, item);
             }
             Err(e) => {
-                if !(e == Error::unsupported_none() && value_serializer.is_none) {
+                if !(e == Error::unsupported_none() && is_none) {
                     return Err(e);
                 }
             }
@@ -199,15 +201,16 @@ impl serde::ser::SerializeStruct for SerializeInlineTable {
     where
         T: serde::ser::Serialize + ?Sized,
     {
-        let mut value_serializer = MapValueSerializer::new();
-        let res = value.serialize(&mut value_serializer);
+        let mut is_none = false;
+        let value_serializer = MapValueSerializer::new(&mut is_none);
+        let res = value.serialize(value_serializer);
         match res {
             Ok(item) => {
                 let item = crate::Item::Value(item);
                 self.items.insert(crate::Key::new(key), item);
             }
             Err(e) => {
-                if !(e == Error::unsupported_none() && value_serializer.is_none) {
+                if !(e == Error::unsupported_none() && is_none) {
                     return Err(e);
                 }
             }
@@ -391,17 +394,17 @@ impl serde::ser::Serializer for DatetimeFieldSerializer {
     }
 }
 
-struct MapValueSerializer {
-    is_none: bool,
+struct MapValueSerializer<'d> {
+    is_none: &'d mut bool,
 }
 
-impl MapValueSerializer {
-    fn new() -> Self {
-        Self { is_none: false }
+impl<'d> MapValueSerializer<'d> {
+    fn new(is_none: &'d mut bool) -> Self {
+        Self { is_none }
     }
 }
 
-impl serde::ser::Serializer for &mut MapValueSerializer {
+impl serde::ser::Serializer for MapValueSerializer<'_> {
     type Ok = crate::Value;
     type Error = Error;
     type SerializeSeq = SerializeValueArray;
@@ -469,7 +472,7 @@ impl serde::ser::Serializer for &mut MapValueSerializer {
     }
 
     fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
-        self.is_none = true;
+        *self.is_none = true;
         Err(Error::unsupported_none())
     }
 
@@ -499,13 +502,13 @@ impl serde::ser::Serializer for &mut MapValueSerializer {
 
     fn serialize_newtype_struct<T>(
         self,
-        name: &'static str,
+        _name: &'static str,
         value: &T,
     ) -> Result<Self::Ok, Self::Error>
     where
         T: serde::ser::Serialize + ?Sized,
     {
-        ValueSerializer::new().serialize_newtype_struct(name, value)
+        value.serialize(self)
     }
 
     fn serialize_newtype_variant<T>(
@@ -570,55 +573,21 @@ impl serde::ser::Serializer for &mut MapValueSerializer {
     }
 }
 
-pub(crate) type SerializeTupleVariant = SerializeVariant<SerializeValueArray>;
-pub(crate) type SerializeStructVariant = SerializeVariant<SerializeMap>;
-
-pub struct SerializeVariant<T> {
+pub struct SerializeStructVariant {
     variant: &'static str,
-    inner: T,
-}
-
-impl SerializeTupleVariant {
-    pub(crate) fn tuple(variant: &'static str, len: usize) -> Self {
-        Self {
-            variant,
-            inner: SerializeValueArray::with_capacity(len),
-        }
-    }
+    inner: SerializeInlineTable,
 }
 
 impl SerializeStructVariant {
     pub(crate) fn struct_(variant: &'static str, len: usize) -> Self {
         Self {
             variant,
-            inner: SerializeMap::table_with_capacity(len),
+            inner: SerializeInlineTable::with_capacity(len),
         }
     }
 }
 
-impl serde::ser::SerializeTupleVariant for SerializeVariant<SerializeValueArray> {
-    type Ok = crate::Value;
-    type Error = Error;
-
-    fn serialize_field<T>(&mut self, value: &T) -> Result<(), Error>
-    where
-        T: serde::ser::Serialize + ?Sized,
-    {
-        serde::ser::SerializeSeq::serialize_element(&mut self.inner, value)
-    }
-
-    fn end(self) -> Result<Self::Ok, Self::Error> {
-        let inner = serde::ser::SerializeSeq::end(self.inner)?;
-        let mut items = crate::table::KeyValuePairs::new();
-        let value = crate::Item::Value(inner);
-        items.insert(crate::Key::new(self.variant), value);
-        Ok(crate::Value::InlineTable(crate::InlineTable::with_pairs(
-            items,
-        )))
-    }
-}
-
-impl serde::ser::SerializeStructVariant for SerializeVariant<SerializeMap> {
+impl serde::ser::SerializeStructVariant for SerializeStructVariant {
     type Ok = crate::Value;
     type Error = Error;
 
@@ -632,7 +601,7 @@ impl serde::ser::SerializeStructVariant for SerializeVariant<SerializeMap> {
 
     #[inline]
     fn end(self) -> Result<Self::Ok, Self::Error> {
-        let inner = serde::ser::SerializeStruct::end(self.inner)?;
+        let inner = serde::ser::SerializeStruct::end(self.inner)?.into();
         let mut items = crate::table::KeyValuePairs::new();
         let value = crate::Item::Value(inner);
         items.insert(crate::Key::new(self.variant), value);
diff --git a/crates/toml_edit/src/ser/value.rs b/crates/toml_edit/src/ser/value.rs
index 7898c5f1..092e5cdc 100644
--- a/crates/toml_edit/src/ser/value.rs
+++ b/crates/toml_edit/src/ser/value.rs
@@ -64,7 +64,7 @@ impl serde::ser::Serializer for ValueSerializer {
     type SerializeSeq = super::array::SerializeValueArray;
     type SerializeTuple = super::array::SerializeValueArray;
     type SerializeTupleStruct = super::array::SerializeValueArray;
-    type SerializeTupleVariant = super::map::SerializeTupleVariant;
+    type SerializeTupleVariant = super::array::SerializeTupleVariant;
     type SerializeMap = super::map::SerializeMap;
     type SerializeStruct = super::map::SerializeMap;
     type SerializeStructVariant = super::map::SerializeStructVariant;
@@ -222,7 +222,7 @@ impl serde::ser::Serializer for ValueSerializer {
         variant: &'static str,
         len: usize,
     ) -> Result<Self::SerializeTupleVariant, Self::Error> {
-        Ok(super::map::SerializeTupleVariant::tuple(variant, len))
+        Ok(super::array::SerializeTupleVariant::tuple(variant, len))
     }
 
     fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
diff --git a/crates/toml_write/src/lib.rs b/crates/toml_write/src/lib.rs
index 53068da2..16df0f9a 100644
--- a/crates/toml_write/src/lib.rs
+++ b/crates/toml_write/src/lib.rs
@@ -1,5 +1,25 @@
 //! A low-level interface for writing out TOML
 //!
+//! Considerations when serializing arbitrary data:
+//! - Verify the implementation with [`toml-test-harness`](https://docs.rs/toml-test-harness)
+//! - Be sure to group keys under a table before writing another table
+//! - Watch for extra trailing newlines and leading newlines, both when starting with top-level
+//!   keys or a table
+//! - When serializing an array-of-tables, be sure to verify that all elements of the array
+//!   serialize as tables
+//! - Standard tables and inline tables may need separate implementations of corner cases,
+//!   requiring verifying them both
+//!
+//! When serializing Rust data structures
+//! - `Option`: Skip key-value pairs with a value of `None`, otherwise error when seeing `None`
+//!   - When skipping key-value pairs, be careful that a deeply nested `None` doesn't get skipped
+//! - Scalars and arrays are unsupported as top-level data types
+//! - Tuples and tuple variants seriallize as arrays
+//! - Structs, struct variants, and maps serialize as tables
+//! - Newtype variants serialize as to the inner type
+//! - Unit variants serialize to a string
+//! - Unit and unit structs don't have a clear meaning in TOML
+//!
 //! # Example
 //!
 //! ```rust


Test patch:
diff --git a/crates/toml/tests/serde/general.rs b/crates/toml/tests/serde/general.rs
index 3a872dba..5771f888 100644
--- a/crates/toml/tests/serde/general.rs
+++ b/crates/toml/tests/serde/general.rs
@@ -175,43 +175,6 @@ fn array() {
 }
 
 #[test]
-fn inner_structs_with_options() {
-    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
-    struct Foo {
-        a: Option<Box<Foo>>,
-        b: Bar,
-    }
-    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
-    struct Bar {
-        a: String,
-        b: f64,
-    }
-
-    equivalent! {
-        Foo {
-            a: Some(Box::new(Foo {
-                a: None,
-                b: Bar { a: "foo".to_owned(), b: 4.5 },
-            })),
-            b: Bar { a: "bar".to_owned(), b: 1.0 },
-        },
-        map! {
-            a: map! {
-                b: map! {
-                    a: crate::SerdeValue::String("foo".to_owned()),
-                    b: crate::SerdeValue::Float(4.5)
-                }
-            },
-            b: map! {
-                a: crate::SerdeValue::String("bar".to_owned()),
-                b: crate::SerdeValue::Float(1.0)
-            }
-        },
-    }
-}
-
-#[test]
-#[cfg(feature = "preserve_order")]
 fn hashmap() {
     use std::collections::HashSet;
 
@@ -673,26 +636,6 @@ fn empty_arrays() {
     }
 }
 
-#[test]
-fn empty_arrays2() {
-    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
-    struct Foo {
-        a: Option<Vec<Bar>>,
-    }
-    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
-    struct Bar;
-
-    equivalent! {
-        Foo { a: None },
-        map! {},
-    }
-
-    equivalent! {
-        Foo { a: Some(vec![]) },
-        map! { a: crate::SerdeValue::Array(vec![]) },
-    }
-}
-
 #[test]
 fn extra_keys() {
     #[derive(Serialize, Deserialize)]
@@ -791,44 +734,6 @@ fn newtype_key() {
     }
 }
 
-#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
-struct CanBeEmpty {
-    a: Option<String>,
-    b: Option<String>,
-}
-
-#[test]
-fn table_structs_empty() {
-    let input = "[bar]\n\n[baz]\n\n[bazv]\na = \"foo\"\n\n[foo]\n";
-    let value: BTreeMap<String, CanBeEmpty> = crate::from_str(input).unwrap();
-    let mut expected: BTreeMap<String, CanBeEmpty> = BTreeMap::new();
-    expected.insert("bar".to_owned(), CanBeEmpty::default());
-    expected.insert("baz".to_owned(), CanBeEmpty::default());
-    expected.insert(
-        "bazv".to_owned(),
-        CanBeEmpty {
-            a: Some("foo".to_owned()),
-            b: None,
-        },
-    );
-    expected.insert("foo".to_owned(), CanBeEmpty::default());
-    assert_eq!(value, expected);
-    assert_data_eq!(
-        crate::to_string(&value).unwrap(),
-        str![[r#"
-[bar]
-
-[baz]
-
-[bazv]
-a = "foo"
-
-[foo]
-
-"#]]
-    );
-}
-
 #[test]
 fn fixed_size_array() {
     #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
@@ -1512,6 +1417,163 @@ values = [{ Optional = { x = 0, y = 4 } }, "Empty", { Optional = { x = 2 } }, {
 "#]].raw());
 }
 
+#[test]
+fn serialize_struct_with_none_string() {
+    #[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
+    struct CanBeEmpty {
+        a: Option<String>,
+        b: Option<String>,
+    }
+
+    let input = "[bar]
+
+[baz]
+
+[bazv]
+a = \"foo\"
+
+[foo]";
+    let value: BTreeMap<String, CanBeEmpty> = crate::from_str(input).unwrap();
+
+    let mut expected: BTreeMap<String, CanBeEmpty> = BTreeMap::new();
+    expected.insert("bar".to_owned(), CanBeEmpty::default());
+    expected.insert("baz".to_owned(), CanBeEmpty::default());
+    expected.insert(
+        "bazv".to_owned(),
+        CanBeEmpty {
+            a: Some("foo".to_owned()),
+            b: None,
+        },
+    );
+    expected.insert("foo".to_owned(), CanBeEmpty::default());
+
+    assert_eq!(value, expected);
+    assert_data_eq!(
+        crate::to_string(&value).unwrap(),
+        str![[r#"
+[bar]
+
+[baz]
+
+[bazv]
+a = "foo"
+
+[foo]
+
+"#]]
+        .raw()
+    );
+}
+
+#[test]
+fn serialize_struct_with_none_vec() {
+    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
+    struct Foo {
+        a: Option<Vec<Bar>>,
+    }
+    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
+    struct Bar;
+
+    equivalent! {
+        Foo { a: None },
+        map! {},
+    }
+
+    equivalent! {
+        Foo { a: Some(vec![]) },
+        map! { a: crate::SerdeValue::Array(vec![]) },
+    }
+}
+
+#[test]
+fn serialize_struct_with_none_struct() {
+    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
+    struct Foo {
+        a: Option<Box<Foo>>,
+        b: Bar,
+    }
+    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
+    struct Bar {
+        a: String,
+        b: f64,
+    }
+
+    equivalent! {
+        Foo {
+            a: Some(Box::new(Foo {
+                a: None,
+                b: Bar { a: "foo".to_owned(), b: 4.5 },
+            })),
+            b: Bar { a: "bar".to_owned(), b: 1.0 },
+        },
+        map! {
+            a: map! {
+                b: map! {
+                    a: crate::SerdeValue::String("foo".to_owned()),
+                    b: crate::SerdeValue::Float(4.5)
+                }
+            },
+            b: map! {
+                a: crate::SerdeValue::String("bar".to_owned()),
+                b: crate::SerdeValue::Float(1.0)
+            }
+        },
+    }
+}
+
+#[test]
+fn serialize_struct_with_newtype_with_none() {
+    #[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
+    struct CanBeEmpty {
+        #[serde(default)]
+        a: NewType,
+        #[serde(default)]
+        b: NewType,
+    }
+
+    #[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
+    struct NewType(Option<String>);
+
+    let input = "[bar]
+
+[baz]
+
+[bazv]
+a = \"foo\"
+
+[foo]";
+    let value: BTreeMap<String, CanBeEmpty> = crate::from_str(input).unwrap();
+
+    let mut expected: BTreeMap<String, CanBeEmpty> = BTreeMap::new();
+    expected.insert("bar".to_owned(), CanBeEmpty::default());
+    expected.insert("baz".to_owned(), CanBeEmpty::default());
+    expected.insert(
+        "bazv".to_owned(),
+        CanBeEmpty {
+            a: NewType(Some("foo".to_owned())),
+            b: NewType(None),
+        },
+    );
+    expected.insert("foo".to_owned(), CanBeEmpty::default());
+
+    assert_eq!(value, expected);
+    assert_data_eq!(
+        crate::to_string(&value).unwrap(),
+        str![[r#"
+[bar]
+
+[baz]
+
+[bazv]
+a = "foo"
+
+[foo]
+
+"#]]
+        .raw()
+    );
+}
+
 #[test]
 fn span_for_sequence_as_map() {
     #[allow(dead_code)]
diff --git a/crates/toml_edit/tests/serde/general.rs b/crates/toml_edit/tests/serde/general.rs
index 0482f54a..de35a666 100644
--- a/crates/toml_edit/tests/serde/general.rs
+++ b/crates/toml_edit/tests/serde/general.rs
@@ -155,42 +155,6 @@ fn array() {
     };
 }
 
-#[test]
-fn inner_structs_with_options() {
-    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
-    struct Foo {
-        a: Option<Box<Foo>>,
-        b: Bar,
-    }
-    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
-    struct Bar {
-        a: String,
-        b: f64,
-    }
-
-    equivalent! {
-        Foo {
-            a: Some(Box::new(Foo {
-                a: None,
-                b: Bar { a: "foo".to_owned(), b: 4.5 },
-            })),
-            b: Bar { a: "bar".to_owned(), b: 1.0 },
-        },
-        map! {
-            a: map! {
-                b: map! {
-                    a: crate::SerdeValue::String("foo".to_owned()),
-                    b: crate::SerdeValue::Float(4.5)
-                }
-            },
-            b: map! {
-                a: crate::SerdeValue::String("bar".to_owned()),
-                b: crate::SerdeValue::Float(1.0)
-            }
-        },
-    }
-}
-
 #[test]
 fn hashmap() {
     use std::collections::HashSet;
@@ -638,26 +602,6 @@ fn empty_arrays() {
     }
 }
 
-#[test]
-fn empty_arrays2() {
-    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
-    struct Foo {
-        a: Option<Vec<Bar>>,
-    }
-    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
-    struct Bar;
-
-    equivalent! {
-        Foo { a: None },
-        map! {},
-    }
-
-    equivalent! {
-        Foo { a: Some(vec![]) },
-        map! { a: crate::SerdeValue::Array(vec![]) },
-    }
-}
-
 #[test]
 fn extra_keys() {
     #[derive(Serialize, Deserialize)]
@@ -756,41 +700,6 @@ fn newtype_key() {
     }
 }
 
-#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
-struct CanBeEmpty {
-    a: Option<String>,
-    b: Option<String>,
-}
-
-#[test]
-fn table_structs_empty() {
-    let input = "[bar]\n\n[baz]\n\n[bazv]\na = \"foo\"\n\n[foo]\n";
-    let value: BTreeMap<String, CanBeEmpty> = crate::from_str(input).unwrap();
-    let mut expected: BTreeMap<String, CanBeEmpty> = BTreeMap::new();
-    expected.insert("bar".to_owned(), CanBeEmpty::default());
-    expected.insert("baz".to_owned(), CanBeEmpty::default());
-    expected.insert(
-        "bazv".to_owned(),
-        CanBeEmpty {
-            a: Some("foo".to_owned()),
-            b: None,
-        },
-    );
-    expected.insert("foo".to_owned(), CanBeEmpty::default());
-    assert_eq!(value, expected);
-    assert_data_eq!(
-        crate::to_string(&value).unwrap(),
-        str![[r#"
-bar = {}
-baz = {}
-bazv = { a = "foo" }
-foo = {}
-
-"#]]
-        .raw()
-    );
-}
-
 #[test]
 fn fixed_size_array() {
     #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
@@ -1455,6 +1364,155 @@ values = [{ Optional = { x = 0, y = 4 } }, "Empty", { Optional = { x = 2 } }, {
 "#]].raw());
 }
 
+#[test]
+fn serialize_struct_with_none_string() {
+    #[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
+    struct CanBeEmpty {
+        a: Option<String>,
+        b: Option<String>,
+    }
+
+    let input = "[bar]
+
+[baz]
+
+[bazv]
+a = \"foo\"
+
+[foo]";
+    let value: BTreeMap<String, CanBeEmpty> = crate::from_str(input).unwrap();
+
+    let mut expected: BTreeMap<String, CanBeEmpty> = BTreeMap::new();
+    expected.insert("bar".to_owned(), CanBeEmpty::default());
+    expected.insert("baz".to_owned(), CanBeEmpty::default());
+    expected.insert(
+        "bazv".to_owned(),
+        CanBeEmpty {
+            a: Some("foo".to_owned()),
+            b: None,
+        },
+    );
+    expected.insert("foo".to_owned(), CanBeEmpty::default());
+
+    assert_eq!(value, expected);
+    assert_data_eq!(
+        crate::to_string(&value).unwrap(),
+        str![[r#"
+bar = {}
+baz = {}
+bazv = { a = "foo" }
+foo = {}
+
+"#]]
+        .raw()
+    );
+}
+
+#[test]
+fn serialize_struct_with_none_vec() {
+    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
+    struct Foo {
+        a: Option<Vec<Bar>>,
+    }
+    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
+    struct Bar;
+
+    equivalent! {
+        Foo { a: None },
+        map! {},
+    }
+
+    equivalent! {
+        Foo { a: Some(vec![]) },
+        map! { a: crate::SerdeValue::Array(vec![]) },
+    }
+}
+
+#[test]
+fn serialize_struct_with_none_struct() {
+    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
+    struct Foo {
+        a: Option<Box<Foo>>,
+        b: Bar,
+    }
+    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
+    struct Bar {
+        a: String,
+        b: f64,
+    }
+
+    equivalent! {
+        Foo {
+            a: Some(Box::new(Foo {
+                a: None,
+                b: Bar { a: "foo".to_owned(), b: 4.5 },
+            })),
+            b: Bar { a: "bar".to_owned(), b: 1.0 },
+        },
+        map! {
+            a: map! {
+                b: map! {
+                    a: crate::SerdeValue::String("foo".to_owned()),
+                    b: crate::SerdeValue::Float(4.5)
+                }
+            },
+            b: map! {
+                a: crate::SerdeValue::String("bar".to_owned()),
+                b: crate::SerdeValue::Float(1.0)
+            }
+        },
+    }
+}
+
+#[test]
+fn serialize_struct_with_newtype_with_none() {
+    #[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
+    struct CanBeEmpty {
+        #[serde(default)]
+        a: NewType,
+        #[serde(default)]
+        b: NewType,
+    }
+
+    #[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
+    struct NewType(Option<String>);
+
+    let input = "[bar]
+
+[baz]
+
+[bazv]
+a = \"foo\"
+
+[foo]";
+    let value: BTreeMap<String, CanBeEmpty> = crate::from_str(input).unwrap();
+
+    let mut expected: BTreeMap<String, CanBeEmpty> = BTreeMap::new();
+    expected.insert("bar".to_owned(), CanBeEmpty::default());
+    expected.insert("baz".to_owned(), CanBeEmpty::default());
+    expected.insert(
+        "bazv".to_owned(),
+        CanBeEmpty {
+            a: NewType(Some("foo".to_owned())),
+            b: NewType(None),
+        },
+    );
+    expected.insert("foo".to_owned(), CanBeEmpty::default());
+
+    assert_eq!(value, expected);
+    assert_data_eq!(
+        crate::to_string(&value).unwrap(),
+        str![[r#"
+bar = {}
+baz = {}
+bazv = { a = "foo" }
+foo = {}
+
+"#]]
+        .raw()
+    );
+}
+
 #[test]
 fn span_for_sequence_as_map() {
     #[allow(dead_code)]