Initial serde-kdl crate
Serde serialization/deserialization for KDL, built on kdl 6.5 (KDL v2 spec).

Public API:
- from_str / from_doc: deserialize from KDL string or KdlDocument
- to_string / to_string_pretty / to_doc: serialize to KDL

Mapping rules:
- Struct fields <-> nodes (field_name value)
- Nested structs <-> children blocks
- Vec<primitive> <-> multiple arguments on one node
- Vec<struct> <-> repeated nodes with same name
- Tuples <-> multiple arguments
- Option::None <-> absent node
- Enums: unit variants as strings, struct/newtype/tuple via child nodes
- HashMap/BTreeMap <-> same as structs
- Dash (-) children convention supported for deserialization

31 integration tests + 1 doctest, all passing.
change zttlwrpoqzzxstsxmomlqwulnqklszkm
commit 526affddf5a433be246390ab8224383dd92f54ad
author Alpha Chen <alpha@kejadlen.dev>
date
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eb5a316
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+target
diff --git a/.ramekin/Dockerfile b/.ramekin/Dockerfile
new file mode 100644
index 0000000..92b1a5e
--- /dev/null
+++ b/.ramekin/Dockerfile
@@ -0,0 +1,21 @@
+FROM ramekin-agent
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    bc \
+    && rm -rf /var/lib/apt/lists/*
+
+# Rust nightly (edition 2024)
+ENV RUSTUP_HOME=/usr/local/rustup \
+    CARGO_HOME=/usr/local/cargo \
+    PATH="/usr/local/cargo/bin:$PATH"
+RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
+    | sh -s -- -y --default-toolchain nightly --profile minimal \
+    && rustup component add llvm-tools-preview \
+    && cargo install grcov
+
+
+ENV RANGER_DEFAULT_BACKLOG=ranger
+
+WORKDIR /workspace
+
+ENTRYPOINT ["pi"]
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..2ad3f03
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,224 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "kdl"
+version = "6.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81a29e7b50079ff44549f68c0becb1c73d7f6de2a4ea952da77966daf3d4761e"
+dependencies = [
+ "miette",
+ "num",
+ "winnow",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "miette"
+version = "7.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
+dependencies = [
+ "cfg-if",
+ "unicode-width",
+]
+
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-kdl"
+version = "0.1.0"
+dependencies = [
+ "kdl",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "winnow"
+version = "0.6.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
+dependencies = [
+ "memchr",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..28b8fac
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "serde-kdl"
+version = "0.1.0"
+edition = "2021"
+description = "Serde integration for KDL (KDL Document Language)"
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/kdl-org/kdl-rs"
+keywords = ["kdl", "serde", "serialization", "config"]
+categories = ["config", "encoding"]
+
+[dependencies]
+kdl = "6.5"
+serde = { version = "1", features = ["derive"] }
+thiserror = "2"
+
+[dev-dependencies]
+serde = { version = "1", features = ["derive"] }
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..83577c2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,166 @@
+# serde-kdl
+
+[Serde](https://serde.rs) integration for [KDL](https://kdl.dev) (KDL Document Language).
+
+Built on top of [`kdl`](https://crates.io/crates/kdl) v6.5 (KDL v2 spec).
+
+## Quick Start
+
+```rust
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Config {
+    title: String,
+    count: i32,
+    enabled: bool,
+}
+
+let kdl_input = r#"
+title "My App"
+count 42
+enabled #true
+"#;
+
+// Deserialize
+let config: Config = serde_kdl::from_str(kdl_input).unwrap();
+
+// Serialize
+let output = serde_kdl::to_string(&config).unwrap();
+
+// Roundtrip
+let roundtrip: Config = serde_kdl::from_str(&output).unwrap();
+assert_eq!(config, roundtrip);
+```
+
+## Mapping Rules
+
+### Structs → Nodes
+
+Each struct field becomes a KDL node. The node name is the field name, and the
+value is the first argument.
+
+```kdl
+title "My App"
+count 42
+enabled #true
+```
+
+### Nested Structs → Children Blocks
+
+```kdl
+server {
+    host "localhost"
+    port 8080
+}
+```
+
+### Vec of Primitives → Multiple Arguments
+
+```kdl
+tags "web" "rust" "config"
+```
+
+### Vec of Structs → Repeated Nodes
+
+```kdl
+server {
+    host "localhost"
+    port 8080
+}
+server {
+    host "example.com"
+    port 443
+}
+```
+
+### Dash Children Convention
+
+For deserialization, the `-` (dash) node name convention is supported:
+
+```kdl
+items {
+    - 1
+    - 2
+    - 3
+}
+```
+
+### Tuples → Multiple Arguments
+
+```kdl
+point 1.0 2.0 3.0
+```
+
+### Option
+
+`None` is represented by the absence of a node. Serialization omits `None`
+fields entirely. A `#null` argument also deserializes as `None`.
+
+### Enums
+
+**Unit variants** serialize as strings:
+
+```kdl
+color "Red"
+```
+
+**Newtype variants** use the variant name as a child node name:
+
+```kdl
+value {
+    Text "hello"
+}
+```
+
+**Struct variants** use the variant name as a child node with a children block:
+
+```kdl
+shape {
+    Circle {
+        radius 5.0
+    }
+}
+```
+
+**Tuple variants** use the variant name as a child node with multiple arguments:
+
+```kdl
+data {
+    Point 1.0 2.0 3.0
+}
+```
+
+### HashMap / BTreeMap
+
+Maps serialize identically to structs — each key becomes a node name:
+
+```kdl
+settings {
+    key1 "value1"
+    key2 "value2"
+}
+```
+
+## API
+
+```rust
+// Deserialize from string
+let config: Config = serde_kdl::from_str(kdl_str)?;
+
+// Deserialize from KdlDocument
+let config: Config = serde_kdl::from_doc(&doc)?;
+
+// Serialize to string
+let s: String = serde_kdl::to_string(&config)?;
+
+// Serialize to string (auto-formatted)
+let s: String = serde_kdl::to_string_pretty(&config)?;
+
+// Serialize to KdlDocument
+let doc: kdl::KdlDocument = serde_kdl::to_doc(&config)?;
+```
+
+## License
+
+MIT OR Apache-2.0
diff --git a/src/de.rs b/src/de.rs
new file mode 100644
index 0000000..d38eae3
--- /dev/null
+++ b/src/de.rs
@@ -0,0 +1,1258 @@
+//! Deserializer implementation for KDL.
+//!
+//! # Mapping Rules
+//!
+//! - **Struct/Map** ↔ KDL document (or children block). Each node name is a key.
+//! - **Primitives** ↔ A node's first argument: `name "value"` or `count 42`.
+//! - **Nested struct** ↔ A node's children block: `nested { field "val" }`.
+//! - **Vec/sequence** ↔ Either multiple nodes with the same name, or a single
+//!   node with multiple arguments, or `-` children convention.
+//! - **Option** ↔ Absent node = `None`, present node = `Some`.
+//! - **Enum (unit variant)** ↔ String argument: `color "Red"`.
+//! - **Enum (newtype/tuple/struct)** ↔ Child node named after variant:
+//!   `shape { Circle { radius 5.0 } }`.
+
+use kdl::{KdlDocument, KdlNode, KdlValue};
+use serde::de::{self, DeserializeSeed, IntoDeserializer, MapAccess, SeqAccess, Visitor};
+use serde::Deserialize;
+
+use crate::error::{Error, Result};
+
+/// Deserialize a type from a KDL string.
+pub fn from_str<'de, T: Deserialize<'de>>(s: &'de str) -> Result<T> {
+    let doc: KdlDocument = s.parse()?;
+    from_doc(&doc)
+}
+
+/// Deserialize a type from a [`KdlDocument`].
+pub fn from_doc<'de, T: Deserialize<'de>>(doc: &KdlDocument) -> Result<T> {
+    let de = DocumentDeserializer::new(doc);
+    T::deserialize(de)
+}
+
+// ---------------------------------------------------------------------------
+// DocumentDeserializer: deserializes a KdlDocument as a struct/map
+// ---------------------------------------------------------------------------
+
+struct DocumentDeserializer<'a> {
+    doc: &'a KdlDocument,
+}
+
+impl<'a> DocumentDeserializer<'a> {
+    fn new(doc: &'a KdlDocument) -> Self {
+        Self { doc }
+    }
+}
+
+impl<'de, 'a> de::Deserializer<'de> for DocumentDeserializer<'a> {
+    type Error = Error;
+
+    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        self.deserialize_map(visitor)
+    }
+
+    fn deserialize_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        _fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value> {
+        visitor.visit_map(DocumentMapAccess::new(self.doc))
+    }
+
+    fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_map(DocumentMapAccess::new(self.doc))
+    }
+
+    fn deserialize_newtype_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value> {
+        visitor.visit_newtype_struct(self)
+    }
+
+    fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_unit()
+    }
+
+    fn deserialize_unit_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value> {
+        visitor.visit_unit()
+    }
+
+    fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_unit()
+    }
+
+    serde::forward_to_deserialize_any! {
+        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64
+        char str string bytes byte_buf option
+        seq tuple tuple_struct enum identifier
+    }
+}
+
+// ---------------------------------------------------------------------------
+// DocumentMapAccess: iterates over unique node names in a document
+// ---------------------------------------------------------------------------
+
+struct DocumentMapAccess<'a> {
+    doc: &'a KdlDocument,
+    /// Unique node names, in order of first appearance
+    keys: Vec<&'a str>,
+    index: usize,
+}
+
+impl<'a> DocumentMapAccess<'a> {
+    fn new(doc: &'a KdlDocument) -> Self {
+        let mut keys = Vec::new();
+        let mut seen = std::collections::HashSet::new();
+        for node in doc.nodes() {
+            let name = node.name().value();
+            if seen.insert(name) {
+                keys.push(name);
+            }
+        }
+        Self {
+            doc,
+            keys,
+            index: 0,
+        }
+    }
+}
+
+impl<'de, 'a> MapAccess<'de> for DocumentMapAccess<'a> {
+    type Error = Error;
+
+    fn next_key_seed<K: DeserializeSeed<'de>>(&mut self, seed: K) -> Result<Option<K::Value>> {
+        if self.index >= self.keys.len() {
+            return Ok(None);
+        }
+        let key = self.keys[self.index];
+        seed.deserialize(key.into_deserializer()).map(Some)
+    }
+
+    fn next_value_seed<V: DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value> {
+        let key = self.keys[self.index];
+        self.index += 1;
+
+        // Collect all nodes with this name
+        let nodes: Vec<&KdlNode> = self
+            .doc
+            .nodes()
+            .iter()
+            .filter(|n| n.name().value() == key)
+            .collect();
+
+        seed.deserialize(FieldDeserializer::new(nodes))
+    }
+}
+
+// ---------------------------------------------------------------------------
+// FieldDeserializer: deserializes all nodes matching a field name
+// ---------------------------------------------------------------------------
+
+struct FieldDeserializer<'a> {
+    nodes: Vec<&'a KdlNode>,
+}
+
+impl<'a> FieldDeserializer<'a> {
+    fn new(nodes: Vec<&'a KdlNode>) -> Self {
+        Self { nodes }
+    }
+
+    fn first_node(&self) -> Result<&'a KdlNode> {
+        self.nodes
+            .first()
+            .copied()
+            .ok_or_else(|| Error::Message("expected at least one node".into()))
+    }
+
+    fn first_arg(&self) -> Result<&'a KdlValue> {
+        let node = self.first_node()?;
+        first_arg_of(node)
+    }
+}
+
+fn first_arg_of<'a>(node: &'a KdlNode) -> Result<&'a KdlValue> {
+    node.get(0).ok_or_else(|| {
+        Error::TypeMismatch {
+            expected: "a value argument",
+            got: format!("node '{}' with no arguments", node.name().value()),
+        }
+    })
+}
+
+/// Helper: get all positional arguments from a node (entries with no name).
+fn node_args(node: &KdlNode) -> Vec<&KdlValue> {
+    node.entries()
+        .iter()
+        .filter(|e| e.name().is_none())
+        .map(|e| e.value())
+        .collect()
+}
+
+/// Helper: get all property entries from a node (entries with a name).
+fn node_props(node: &KdlNode) -> Vec<(&str, &KdlValue)> {
+    node.entries()
+        .iter()
+        .filter_map(|e| e.name().map(|n| (n.value(), e.value())))
+        .collect()
+}
+
+impl<'de, 'a> de::Deserializer<'de> for FieldDeserializer<'a> {
+    type Error = Error;
+
+    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        let node = self.first_node()?;
+        let args = node_args(node);
+        let props = node_props(node);
+
+        if node.children().is_some() {
+            // Has children → treat as struct/map
+            self.deserialize_map(visitor)
+        } else if !props.is_empty() {
+            // Has properties → treat as map
+            self.deserialize_map(visitor)
+        } else if args.len() == 1 {
+            // Single argument → deserialize as value
+            ValueDeserializer::new(args[0]).deserialize_any(visitor)
+        } else if args.len() > 1 {
+            // Multiple arguments → deserialize as seq
+            self.deserialize_seq(visitor)
+        } else {
+            // No args, no children, no props → null/unit
+            visitor.visit_unit()
+        }
+    }
+
+    // -- Primitives: delegate to first argument --
+
+    fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_bool(visitor)
+    }
+
+    fn deserialize_i8<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_i8(visitor)
+    }
+
+    fn deserialize_i16<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_i16(visitor)
+    }
+
+    fn deserialize_i32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_i32(visitor)
+    }
+
+    fn deserialize_i64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_i64(visitor)
+    }
+
+    fn deserialize_i128<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_i128(visitor)
+    }
+
+    fn deserialize_u8<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_u8(visitor)
+    }
+
+    fn deserialize_u16<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_u16(visitor)
+    }
+
+    fn deserialize_u32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_u32(visitor)
+    }
+
+    fn deserialize_u64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_u64(visitor)
+    }
+
+    fn deserialize_u128<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_u128(visitor)
+    }
+
+    fn deserialize_f32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_f32(visitor)
+    }
+
+    fn deserialize_f64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_f64(visitor)
+    }
+
+    fn deserialize_char<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_char(visitor)
+    }
+
+    fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_str(visitor)
+    }
+
+    fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(self.first_arg()?).deserialize_string(visitor)
+    }
+
+    fn deserialize_bytes<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        self.deserialize_seq(visitor)
+    }
+
+    fn deserialize_byte_buf<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        self.deserialize_seq(visitor)
+    }
+
+    fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        // If we got here, the node exists, so it's Some.
+        // None is handled at the map level by the absence of a node.
+        let node = self.first_node()?;
+        let args = node_args(node);
+        // If there's a single argument and it's null, treat as None
+        if args.len() == 1 && args[0].is_null() && node.children().is_none() {
+            visitor.visit_none()
+        } else {
+            visitor.visit_some(self)
+        }
+    }
+
+    fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_unit()
+    }
+
+    fn deserialize_unit_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value> {
+        visitor.visit_unit()
+    }
+
+    fn deserialize_newtype_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value> {
+        visitor.visit_newtype_struct(self)
+    }
+
+    fn deserialize_seq<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        // Strategy:
+        // 1. Multiple nodes with same name → each node is an element
+        // 2. Single node with `-` children → each `-` child is an element
+        // 3. Single node with multiple args → each arg is an element
+        // 4. Single node with non-`-` children → children nodes are elements
+
+        if self.nodes.len() > 1 {
+            // Multiple nodes → each node contributes one element
+            visitor.visit_seq(MultiNodeSeqAccess {
+                nodes: self.nodes,
+                index: 0,
+            })
+        } else {
+            let node = self.first_node()?;
+
+            // Check for `-` children convention
+            if let Some(children) = node.children() {
+                let dash_nodes: Vec<&KdlNode> = children
+                    .nodes()
+                    .iter()
+                    .filter(|n| n.name().value() == "-")
+                    .collect();
+                if !dash_nodes.is_empty() {
+                    return visitor.visit_seq(MultiNodeSeqAccess {
+                        nodes: dash_nodes,
+                        index: 0,
+                    });
+                }
+                // Non-dash children: treat each child as an element
+                let child_nodes: Vec<&KdlNode> = children.nodes().iter().collect();
+                if !child_nodes.is_empty() {
+                    return visitor.visit_seq(ChildNodeSeqAccess {
+                        nodes: child_nodes,
+                        index: 0,
+                    });
+                }
+            }
+
+            // Arguments as elements
+            let args = node_args(node);
+            visitor.visit_seq(ArgsSeqAccess {
+                args,
+                index: 0,
+            })
+        }
+    }
+
+    fn deserialize_tuple<V: Visitor<'de>>(self, _len: usize, visitor: V) -> Result<V::Value> {
+        self.deserialize_seq(visitor)
+    }
+
+    fn deserialize_tuple_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        _len: usize,
+        visitor: V,
+    ) -> Result<V::Value> {
+        self.deserialize_seq(visitor)
+    }
+
+    fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        let node = self.first_node()?;
+        // If node has children, deserialize from children document
+        if let Some(children) = node.children() {
+            return visitor.visit_map(DocumentMapAccess::new(children));
+        }
+        // If node has properties, deserialize from properties
+        let props = node_props(node);
+        if !props.is_empty() {
+            return visitor.visit_map(PropsMapAccess {
+                props,
+                index: 0,
+            });
+        }
+        // Empty map
+        visitor.visit_map(PropsMapAccess {
+            props: vec![],
+            index: 0,
+        })
+    }
+
+    fn deserialize_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value> {
+        let node = self.first_node()?;
+
+        // If node has children, deserialize struct from children
+        if let Some(children) = node.children() {
+            return visitor.visit_map(DocumentMapAccess::new(children));
+        }
+
+        // If node has properties, use them as struct fields
+        let props = node_props(node);
+        if !props.is_empty() {
+            return visitor.visit_map(PropsMapAccess {
+                props,
+                index: 0,
+            });
+        }
+
+        // If struct has a single field and node has a single argument,
+        // try to match argument to the sole field
+        let args = node_args(node);
+        if fields.len() == 1 && args.len() == 1 {
+            return visitor.visit_map(SingleArgStructAccess {
+                field_name: fields[0],
+                value: args[0],
+                done: false,
+            });
+        }
+
+        // Empty
+        visitor.visit_map(PropsMapAccess {
+            props: vec![],
+            index: 0,
+        })
+    }
+
+    fn deserialize_enum<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        _variants: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value> {
+        let node = self.first_node()?;
+        let args = node_args(node);
+
+        // Unit variant: first argument is variant name string
+        // e.g., `color "Red"`
+        if !args.is_empty() && node.children().is_none() {
+            if let Some(s) = args[0].as_string() {
+                return visitor.visit_enum(EnumUnitAccess {
+                    variant: s,
+                    node,
+                    arg_offset: 1,
+                });
+            }
+        }
+
+        // Complex variant: child node named after variant
+        // e.g., `shape { Circle { radius 5.0 } }`
+        if let Some(children) = node.children() {
+            let child_nodes = children.nodes();
+            if child_nodes.len() == 1 {
+                let variant_node = &child_nodes[0];
+                return visitor.visit_enum(EnumComplexAccess {
+                    variant_name: variant_node.name().value(),
+                    variant_node,
+                });
+            }
+        }
+
+        Err(Error::Message(format!(
+            "cannot deserialize enum from node '{}'",
+            node.name().value()
+        )))
+    }
+
+    fn deserialize_identifier<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        self.deserialize_str(visitor)
+    }
+
+    fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_unit()
+    }
+}
+
+// ---------------------------------------------------------------------------
+// SeqAccess implementations
+// ---------------------------------------------------------------------------
+
+/// Seq from multiple nodes with the same name. Each node is deserialized as an element.
+struct MultiNodeSeqAccess<'a> {
+    nodes: Vec<&'a KdlNode>,
+    index: usize,
+}
+
+impl<'de, 'a> SeqAccess<'de> for MultiNodeSeqAccess<'a> {
+    type Error = Error;
+
+    fn next_element_seed<T: DeserializeSeed<'de>>(&mut self, seed: T) -> Result<Option<T::Value>> {
+        if self.index >= self.nodes.len() {
+            return Ok(None);
+        }
+        let node = self.nodes[self.index];
+        self.index += 1;
+        seed.deserialize(NodeContentDeserializer::new(node))
+            .map(Some)
+    }
+}
+
+/// Seq from child nodes (non-dash). Each child node is an element deserialized
+/// by its content (children or first arg).
+struct ChildNodeSeqAccess<'a> {
+    nodes: Vec<&'a KdlNode>,
+    index: usize,
+}
+
+impl<'de, 'a> SeqAccess<'de> for ChildNodeSeqAccess<'a> {
+    type Error = Error;
+
+    fn next_element_seed<T: DeserializeSeed<'de>>(&mut self, seed: T) -> Result<Option<T::Value>> {
+        if self.index >= self.nodes.len() {
+            return Ok(None);
+        }
+        let node = self.nodes[self.index];
+        self.index += 1;
+        seed.deserialize(NodeContentDeserializer::new(node))
+            .map(Some)
+    }
+}
+
+/// Seq from a node's positional arguments.
+struct ArgsSeqAccess<'a> {
+    args: Vec<&'a KdlValue>,
+    index: usize,
+}
+
+impl<'de, 'a> SeqAccess<'de> for ArgsSeqAccess<'a> {
+    type Error = Error;
+
+    fn next_element_seed<T: DeserializeSeed<'de>>(&mut self, seed: T) -> Result<Option<T::Value>> {
+        if self.index >= self.args.len() {
+            return Ok(None);
+        }
+        let val = self.args[self.index];
+        self.index += 1;
+        seed.deserialize(ValueDeserializer::new(val)).map(Some)
+    }
+}
+
+// ---------------------------------------------------------------------------
+// PropsMapAccess: iterate over node properties as key-value pairs
+// ---------------------------------------------------------------------------
+
+struct PropsMapAccess<'a> {
+    props: Vec<(&'a str, &'a KdlValue)>,
+    index: usize,
+}
+
+impl<'de, 'a> MapAccess<'de> for PropsMapAccess<'a> {
+    type Error = Error;
+
+    fn next_key_seed<K: DeserializeSeed<'de>>(&mut self, seed: K) -> Result<Option<K::Value>> {
+        if self.index >= self.props.len() {
+            return Ok(None);
+        }
+        let (key, _) = self.props[self.index];
+        seed.deserialize(key.into_deserializer()).map(Some)
+    }
+
+    fn next_value_seed<V: DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value> {
+        let (_, val) = self.props[self.index];
+        self.index += 1;
+        seed.deserialize(ValueDeserializer::new(val))
+    }
+}
+
+// ---------------------------------------------------------------------------
+// SingleArgStructAccess: a struct with one field from a node's single arg
+// ---------------------------------------------------------------------------
+
+struct SingleArgStructAccess<'a> {
+    field_name: &'static str,
+    value: &'a KdlValue,
+    done: bool,
+}
+
+impl<'de, 'a> MapAccess<'de> for SingleArgStructAccess<'a> {
+    type Error = Error;
+
+    fn next_key_seed<K: DeserializeSeed<'de>>(&mut self, seed: K) -> Result<Option<K::Value>> {
+        if self.done {
+            return Ok(None);
+        }
+        seed.deserialize(self.field_name.into_deserializer())
+            .map(Some)
+    }
+
+    fn next_value_seed<V: DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value> {
+        self.done = true;
+        seed.deserialize(ValueDeserializer::new(self.value))
+    }
+}
+
+// ---------------------------------------------------------------------------
+// NodeContentDeserializer: deserializes from a single node's content
+// ---------------------------------------------------------------------------
+
+/// Deserializes a single node's "content" - its arguments, properties, and children.
+/// Used when a node represents a single value in a sequence.
+struct NodeContentDeserializer<'a> {
+    node: &'a KdlNode,
+}
+
+impl<'a> NodeContentDeserializer<'a> {
+    fn new(node: &'a KdlNode) -> Self {
+        Self { node }
+    }
+}
+
+impl<'de, 'a> de::Deserializer<'de> for NodeContentDeserializer<'a> {
+    type Error = Error;
+
+    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        let args = node_args(self.node);
+        let props = node_props(self.node);
+
+        if self.node.children().is_some() {
+            self.deserialize_map(visitor)
+        } else if !props.is_empty() {
+            self.deserialize_map(visitor)
+        } else if args.len() == 1 {
+            ValueDeserializer::new(args[0]).deserialize_any(visitor)
+        } else if args.len() > 1 {
+            self.deserialize_seq(visitor)
+        } else {
+            visitor.visit_unit()
+        }
+    }
+
+    fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_bool(visitor)
+    }
+
+    fn deserialize_i8<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_i8(visitor)
+    }
+
+    fn deserialize_i16<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_i16(visitor)
+    }
+
+    fn deserialize_i32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_i32(visitor)
+    }
+
+    fn deserialize_i64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_i64(visitor)
+    }
+
+    fn deserialize_i128<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_i128(visitor)
+    }
+
+    fn deserialize_u8<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_u8(visitor)
+    }
+
+    fn deserialize_u16<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_u16(visitor)
+    }
+
+    fn deserialize_u32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_u32(visitor)
+    }
+
+    fn deserialize_u64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_u64(visitor)
+    }
+
+    fn deserialize_u128<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_u128(visitor)
+    }
+
+    fn deserialize_f32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_f32(visitor)
+    }
+
+    fn deserialize_f64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_f64(visitor)
+    }
+
+    fn deserialize_char<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_char(visitor)
+    }
+
+    fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_str(visitor)
+    }
+
+    fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        ValueDeserializer::new(first_arg_of(self.node)?).deserialize_string(visitor)
+    }
+
+    fn deserialize_bytes<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        self.deserialize_seq(visitor)
+    }
+
+    fn deserialize_byte_buf<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        self.deserialize_seq(visitor)
+    }
+
+    fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        let args = node_args(self.node);
+        if args.len() == 1 && args[0].is_null() && self.node.children().is_none() {
+            visitor.visit_none()
+        } else {
+            visitor.visit_some(self)
+        }
+    }
+
+    fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_unit()
+    }
+
+    fn deserialize_unit_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value> {
+        visitor.visit_unit()
+    }
+
+    fn deserialize_newtype_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value> {
+        visitor.visit_newtype_struct(self)
+    }
+
+    fn deserialize_seq<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        // Check for `-` children convention first
+        if let Some(children) = self.node.children() {
+            let dash_nodes: Vec<&KdlNode> = children
+                .nodes()
+                .iter()
+                .filter(|n| n.name().value() == "-")
+                .collect();
+            if !dash_nodes.is_empty() {
+                return visitor.visit_seq(MultiNodeSeqAccess {
+                    nodes: dash_nodes,
+                    index: 0,
+                });
+            }
+            // Non-dash children
+            let child_nodes: Vec<&KdlNode> = children.nodes().iter().collect();
+            if !child_nodes.is_empty() {
+                return visitor.visit_seq(ChildNodeSeqAccess {
+                    nodes: child_nodes,
+                    index: 0,
+                });
+            }
+        }
+        // Fall back to arguments
+        let args = node_args(self.node);
+        visitor.visit_seq(ArgsSeqAccess { args, index: 0 })
+    }
+
+    fn deserialize_tuple<V: Visitor<'de>>(self, _len: usize, visitor: V) -> Result<V::Value> {
+        self.deserialize_seq(visitor)
+    }
+
+    fn deserialize_tuple_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        _len: usize,
+        visitor: V,
+    ) -> Result<V::Value> {
+        self.deserialize_seq(visitor)
+    }
+
+    fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        if let Some(children) = self.node.children() {
+            return visitor.visit_map(DocumentMapAccess::new(children));
+        }
+        let props = node_props(self.node);
+        visitor.visit_map(PropsMapAccess {
+            props,
+            index: 0,
+        })
+    }
+
+    fn deserialize_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value> {
+        if let Some(children) = self.node.children() {
+            return visitor.visit_map(DocumentMapAccess::new(children));
+        }
+        let props = node_props(self.node);
+        if !props.is_empty() {
+            return visitor.visit_map(PropsMapAccess {
+                props,
+                index: 0,
+            });
+        }
+        // Single-arg struct
+        let args = node_args(self.node);
+        if fields.len() == 1 && args.len() == 1 {
+            return visitor.visit_map(SingleArgStructAccess {
+                field_name: fields[0],
+                value: args[0],
+                done: false,
+            });
+        }
+        visitor.visit_map(PropsMapAccess {
+            props: vec![],
+            index: 0,
+        })
+    }
+
+    fn deserialize_enum<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        _variants: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value> {
+        let args = node_args(self.node);
+
+        // Unit variant: first argument is variant name
+        if !args.is_empty() && self.node.children().is_none() {
+            if let Some(s) = args[0].as_string() {
+                return visitor.visit_enum(EnumUnitAccess {
+                    variant: s,
+                    node: self.node,
+                    arg_offset: 1,
+                });
+            }
+        }
+
+        // Complex variant: child node named after variant
+        if let Some(children) = self.node.children() {
+            let child_nodes = children.nodes();
+            if child_nodes.len() == 1 {
+                let variant_node = &child_nodes[0];
+                return visitor.visit_enum(EnumComplexAccess {
+                    variant_name: variant_node.name().value(),
+                    variant_node,
+                });
+            }
+        }
+
+        Err(Error::Message(format!(
+            "cannot deserialize enum from node '{}'",
+            self.node.name().value()
+        )))
+    }
+
+    fn deserialize_identifier<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        self.deserialize_str(visitor)
+    }
+
+    fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_unit()
+    }
+}
+
+// ---------------------------------------------------------------------------
+// ValueDeserializer: deserializes a KdlValue as a primitive
+// ---------------------------------------------------------------------------
+
+struct ValueDeserializer<'a> {
+    value: &'a KdlValue,
+}
+
+impl<'a> ValueDeserializer<'a> {
+    fn new(value: &'a KdlValue) -> Self {
+        Self { value }
+    }
+
+    fn to_i128(&self) -> Result<i128> {
+        match self.value {
+            KdlValue::Integer(i) => Ok(*i),
+            KdlValue::Float(f) => Ok(*f as i128),
+            other => Err(Error::TypeMismatch {
+                expected: "integer",
+                got: format!("{other:?}"),
+            }),
+        }
+    }
+
+    fn to_f64(&self) -> Result<f64> {
+        match self.value {
+            KdlValue::Float(f) => Ok(*f),
+            KdlValue::Integer(i) => Ok(*i as f64),
+            other => Err(Error::TypeMismatch {
+                expected: "float",
+                got: format!("{other:?}"),
+            }),
+        }
+    }
+}
+
+macro_rules! deserialize_integer {
+    ($method:ident, $visit:ident, $ty:ty) => {
+        fn $method<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+            let i = self.to_i128()?;
+            let val: $ty = i.try_into().map_err(|_| Error::IntegerOutOfRange(i))?;
+            visitor.$visit(val)
+        }
+    };
+}
+
+impl<'de, 'a> de::Deserializer<'de> for ValueDeserializer<'a> {
+    type Error = Error;
+
+    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        match self.value {
+            KdlValue::String(s) => visitor.visit_str(s),
+            KdlValue::Integer(i) => visitor.visit_i128(*i),
+            KdlValue::Float(f) => visitor.visit_f64(*f),
+            KdlValue::Bool(b) => visitor.visit_bool(*b),
+            KdlValue::Null => visitor.visit_none(),
+        }
+    }
+
+    fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        match self.value {
+            KdlValue::Bool(b) => visitor.visit_bool(*b),
+            other => Err(Error::TypeMismatch {
+                expected: "bool",
+                got: format!("{other:?}"),
+            }),
+        }
+    }
+
+    deserialize_integer!(deserialize_i8, visit_i8, i8);
+    deserialize_integer!(deserialize_i16, visit_i16, i16);
+    deserialize_integer!(deserialize_i32, visit_i32, i32);
+    deserialize_integer!(deserialize_i64, visit_i64, i64);
+    deserialize_integer!(deserialize_u8, visit_u8, u8);
+    deserialize_integer!(deserialize_u16, visit_u16, u16);
+    deserialize_integer!(deserialize_u32, visit_u32, u32);
+    deserialize_integer!(deserialize_u64, visit_u64, u64);
+
+    fn deserialize_i128<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_i128(self.to_i128()?)
+    }
+
+    fn deserialize_u128<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        let i = self.to_i128()?;
+        let val: u128 = i.try_into().map_err(|_| Error::IntegerOutOfRange(i))?;
+        visitor.visit_u128(val)
+    }
+
+    fn deserialize_f32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_f32(self.to_f64()? as f32)
+    }
+
+    fn deserialize_f64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_f64(self.to_f64()?)
+    }
+
+    fn deserialize_char<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        match self.value {
+            KdlValue::String(s) if s.len() == 1 => {
+                visitor.visit_char(s.chars().next().unwrap())
+            }
+            other => Err(Error::TypeMismatch {
+                expected: "single character",
+                got: format!("{other:?}"),
+            }),
+        }
+    }
+
+    fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        match self.value {
+            KdlValue::String(s) => visitor.visit_str(s),
+            other => Err(Error::TypeMismatch {
+                expected: "string",
+                got: format!("{other:?}"),
+            }),
+        }
+    }
+
+    fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        self.deserialize_str(visitor)
+    }
+
+    fn deserialize_bytes<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        match self.value {
+            KdlValue::String(s) => visitor.visit_bytes(s.as_bytes()),
+            other => Err(Error::TypeMismatch {
+                expected: "string (for bytes)",
+                got: format!("{other:?}"),
+            }),
+        }
+    }
+
+    fn deserialize_byte_buf<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        self.deserialize_bytes(visitor)
+    }
+
+    fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        if self.value.is_null() {
+            visitor.visit_none()
+        } else {
+            visitor.visit_some(self)
+        }
+    }
+
+    fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        if self.value.is_null() {
+            visitor.visit_unit()
+        } else {
+            Err(Error::TypeMismatch {
+                expected: "null",
+                got: format!("{:?}", self.value),
+            })
+        }
+    }
+
+    fn deserialize_unit_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value> {
+        self.deserialize_unit(visitor)
+    }
+
+    fn deserialize_newtype_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value> {
+        visitor.visit_newtype_struct(self)
+    }
+
+    fn deserialize_seq<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Value> {
+        Err(Error::TypeMismatch {
+            expected: "sequence",
+            got: format!("scalar value {:?}", self.value),
+        })
+    }
+
+    fn deserialize_tuple<V: Visitor<'de>>(self, _len: usize, visitor: V) -> Result<V::Value> {
+        self.deserialize_seq(visitor)
+    }
+
+    fn deserialize_tuple_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        _len: usize,
+        visitor: V,
+    ) -> Result<V::Value> {
+        self.deserialize_seq(visitor)
+    }
+
+    fn deserialize_map<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Value> {
+        Err(Error::TypeMismatch {
+            expected: "map",
+            got: format!("scalar value {:?}", self.value),
+        })
+    }
+
+    fn deserialize_struct<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        _fields: &'static [&'static str],
+        _visitor: V,
+    ) -> Result<V::Value> {
+        Err(Error::TypeMismatch {
+            expected: "struct",
+            got: format!("scalar value {:?}", self.value),
+        })
+    }
+
+    fn deserialize_enum<V: Visitor<'de>>(
+        self,
+        _name: &'static str,
+        _variants: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value> {
+        // For a bare value, it must be a string naming a unit variant
+        match self.value {
+            KdlValue::String(s) => visitor.visit_enum(s.as_str().into_deserializer()),
+            other => Err(Error::TypeMismatch {
+                expected: "string (enum variant name)",
+                got: format!("{other:?}"),
+            }),
+        }
+    }
+
+    fn deserialize_identifier<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        self.deserialize_str(visitor)
+    }
+
+    fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+        visitor.visit_unit()
+    }
+}
+
+// ---------------------------------------------------------------------------
+// Enum deserialization
+// ---------------------------------------------------------------------------
+
+/// For unit and newtype variants where the variant name is a string argument.
+/// e.g., `color "Red"` or `wrapper "SomeVariant" 42`
+struct EnumUnitAccess<'a> {
+    variant: &'a str,
+    node: &'a KdlNode,
+    arg_offset: usize,
+}
+
+impl<'de, 'a> de::EnumAccess<'de> for EnumUnitAccess<'a> {
+    type Error = Error;
+    type Variant = EnumUnitVariantAccess<'a>;
+
+    fn variant_seed<V: DeserializeSeed<'de>>(self, seed: V) -> Result<(V::Value, Self::Variant)> {
+        let val = seed.deserialize::<serde::de::value::StrDeserializer<'_, Error>>(
+            self.variant.into_deserializer(),
+        )?;
+        Ok((
+            val,
+            EnumUnitVariantAccess {
+                node: self.node,
+                arg_offset: self.arg_offset,
+            },
+        ))
+    }
+}
+
+struct EnumUnitVariantAccess<'a> {
+    node: &'a KdlNode,
+    arg_offset: usize,
+}
+
+impl<'de, 'a> de::VariantAccess<'de> for EnumUnitVariantAccess<'a> {
+    type Error = Error;
+
+    fn unit_variant(self) -> Result<()> {
+        Ok(())
+    }
+
+    fn newtype_variant_seed<T: DeserializeSeed<'de>>(self, seed: T) -> Result<T::Value> {
+        let args = node_args(self.node);
+        if self.arg_offset < args.len() {
+            seed.deserialize(ValueDeserializer::new(args[self.arg_offset]))
+        } else {
+            Err(Error::Message(
+                "expected a value after variant name for newtype variant".into(),
+            ))
+        }
+    }
+
+    fn tuple_variant<V: Visitor<'de>>(self, _len: usize, visitor: V) -> Result<V::Value> {
+        let args = node_args(self.node);
+        let remaining: Vec<&KdlValue> = args[self.arg_offset..].to_vec();
+        visitor.visit_seq(ArgsSeqAccess {
+            args: remaining,
+            index: 0,
+        })
+    }
+
+    fn struct_variant<V: Visitor<'de>>(
+        self,
+        _fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value> {
+        // Properties of the node are the struct fields
+        let props = node_props(self.node);
+        visitor.visit_map(PropsMapAccess { props, index: 0 })
+    }
+}
+
+/// For complex variants where a child node is named after the variant.
+/// e.g., `shape { Circle { radius 5.0 } }`
+struct EnumComplexAccess<'a> {
+    variant_name: &'a str,
+    variant_node: &'a KdlNode,
+}
+
+impl<'de, 'a> de::EnumAccess<'de> for EnumComplexAccess<'a> {
+    type Error = Error;
+    type Variant = EnumComplexVariantAccess<'a>;
+
+    fn variant_seed<V: DeserializeSeed<'de>>(self, seed: V) -> Result<(V::Value, Self::Variant)> {
+        let val = seed.deserialize::<serde::de::value::StrDeserializer<'_, Error>>(
+            self.variant_name.into_deserializer(),
+        )?;
+        Ok((
+            val,
+            EnumComplexVariantAccess {
+                node: self.variant_node,
+            },
+        ))
+    }
+}
+
+struct EnumComplexVariantAccess<'a> {
+    node: &'a KdlNode,
+}
+
+impl<'de, 'a> de::VariantAccess<'de> for EnumComplexVariantAccess<'a> {
+    type Error = Error;
+
+    fn unit_variant(self) -> Result<()> {
+        Ok(())
+    }
+
+    fn newtype_variant_seed<T: DeserializeSeed<'de>>(self, seed: T) -> Result<T::Value> {
+        seed.deserialize(NodeContentDeserializer::new(self.node))
+    }
+
+    fn tuple_variant<V: Visitor<'de>>(self, _len: usize, visitor: V) -> Result<V::Value> {
+        let args = node_args(self.node);
+        visitor.visit_seq(ArgsSeqAccess { args, index: 0 })
+    }
+
+    fn struct_variant<V: Visitor<'de>>(
+        self,
+        _fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value> {
+        if let Some(children) = self.node.children() {
+            visitor.visit_map(DocumentMapAccess::new(children))
+        } else {
+            let props = node_props(self.node);
+            visitor.visit_map(PropsMapAccess { props, index: 0 })
+        }
+    }
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..f03272c
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,54 @@
+use std::fmt;
+
+/// Error type for serde-kdl serialization and deserialization.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    /// A KDL parsing error.
+    #[error("KDL parse error: {0}")]
+    Parse(#[from] kdl::KdlError),
+
+    /// A serde (de)serialization error.
+    #[error("{0}")]
+    Message(String),
+
+    /// The top-level type must be a struct or map.
+    #[error("top-level type must be a struct or map")]
+    TopLevelNotStruct,
+
+    /// Expected a specific type but got something else.
+    #[error("type mismatch: expected {expected}, got {got}")]
+    TypeMismatch {
+        expected: &'static str,
+        got: String,
+    },
+
+    /// A required field was missing.
+    #[error("missing field: {0}")]
+    MissingField(String),
+
+    /// Integer overflow during conversion.
+    #[error("integer {0} out of range for target type")]
+    IntegerOutOfRange(i128),
+
+    /// Enum variant not found or ambiguous.
+    #[error("unknown enum variant: {0}")]
+    UnknownVariant(String),
+
+    /// Unsupported operation.
+    #[error("unsupported: {0}")]
+    Unsupported(String),
+}
+
+impl serde::de::Error for Error {
+    fn custom<T: fmt::Display>(msg: T) -> Self {
+        Error::Message(msg.to_string())
+    }
+}
+
+impl serde::ser::Error for Error {
+    fn custom<T: fmt::Display>(msg: T) -> Self {
+        Error::Message(msg.to_string())
+    }
+}
+
+pub type Result<T> = std::result::Result<T, Error>;
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..b8d56dd
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,123 @@
+//! # serde-kdl
+//!
+//! [Serde](https://serde.rs) integration for [KDL](https://kdl.dev), the
+//! KDL Document Language.
+//!
+//! This crate provides `serialize` and `deserialize` support for KDL documents
+//! using the [`kdl`](https://crates.io/crates/kdl) crate (v6, KDL v2 spec).
+//!
+//! ## Quick Start
+//!
+//! ```rust
+//! use serde::{Deserialize, Serialize};
+//!
+//! #[derive(Debug, Serialize, Deserialize, PartialEq)]
+//! struct Config {
+//!     title: String,
+//!     count: i32,
+//!     enabled: bool,
+//! }
+//!
+//! let kdl_input = r#"
+//! title "My App"
+//! count 42
+//! enabled #true
+//! "#;
+//!
+//! // Deserialize
+//! let config: Config = serde_kdl::from_str(kdl_input).unwrap();
+//! assert_eq!(config.title, "My App");
+//! assert_eq!(config.count, 42);
+//! assert_eq!(config.enabled, true);
+//!
+//! // Serialize
+//! let output = serde_kdl::to_string(&config).unwrap();
+//! let roundtrip: Config = serde_kdl::from_str(&output).unwrap();
+//! assert_eq!(config, roundtrip);
+//! ```
+//!
+//! ## Mapping Rules
+//!
+//! ### Structs and Maps
+//!
+//! Struct fields map to node names. Each field becomes a node whose name is
+//! the field name and whose first argument is the value.
+//!
+//! ```kdl
+//! title "My App"
+//! count 42
+//! enabled #true
+//! ```
+//!
+//! ### Nested Structs
+//!
+//! Nested structs use children blocks:
+//!
+//! ```kdl
+//! server {
+//!     host "localhost"
+//!     port 8080
+//! }
+//! ```
+//!
+//! ### Sequences
+//!
+//! Sequences of primitives use multiple arguments on a single node:
+//!
+//! ```kdl
+//! tags "web" "rust" "config"
+//! ```
+//!
+//! Sequences of structs use repeated nodes with the same name:
+//!
+//! ```kdl
+//! server {
+//!     host "localhost"
+//!     port 8080
+//! }
+//! server {
+//!     host "example.com"
+//!     port 443
+//! }
+//! ```
+//!
+//! The `-` (dash) children convention is also supported for deserialization:
+//!
+//! ```kdl
+//! items {
+//!     - 1
+//!     - 2
+//!     - 3
+//! }
+//! ```
+//!
+//! ### Option
+//!
+//! `None` is represented by the absence of a node. `Some(value)` serializes
+//! the inner value normally. `#null` arguments also deserialize as `None`.
+//!
+//! ### Enums
+//!
+//! Unit variants serialize as strings:
+//!
+//! ```kdl
+//! color "Red"
+//! ```
+//!
+//! Newtype, tuple, and struct variants use the variant name as a child node:
+//!
+//! ```kdl
+//! shape {
+//!     Circle {
+//!         radius 5.0
+//!     }
+//! }
+//! ```
+
+pub mod de;
+pub mod error;
+pub mod ser;
+
+pub use de::{from_doc, from_str};
+pub use error::Error;
+pub use ser::{to_doc, to_string, to_string_pretty};
diff --git a/src/ser.rs b/src/ser.rs
new file mode 100644
index 0000000..f715388
--- /dev/null
+++ b/src/ser.rs
@@ -0,0 +1,543 @@
+//! Serializer implementation for KDL.
+//!
+//! # Mapping Rules
+//!
+//! - **Struct/Map** → KDL document. Each field becomes a node.
+//! - **Primitives** → Node argument: `name "value"`, `count 42`.
+//! - **Nested struct** → Node with children block: `nested { field "val" }`.
+//! - **Vec of primitives** → Single node, multiple arguments: `tags "a" "b" "c"`.
+//! - **Vec of structs** → Multiple nodes with the same name.
+//! - **Option::None** → Node omitted. **Option::Some** → Serialize inner value.
+//! - **Enum (unit variant)** → String: `"VariantName"`.
+//! - **Enum (newtype)** → Child node: `VariantName value`.
+//! - **Enum (struct)** → Child node with children: `VariantName { field val }`.
+
+use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue};
+use serde::ser::{self, Serialize};
+
+use crate::error::{Error, Result};
+
+/// Serialize a value to a KDL string.
+pub fn to_string<T: Serialize>(value: &T) -> Result<String> {
+    let doc = to_doc(value)?;
+    Ok(doc.to_string())
+}
+
+/// Serialize a value to a KDL string with auto-formatting.
+pub fn to_string_pretty<T: Serialize>(value: &T) -> Result<String> {
+    let mut doc = to_doc(value)?;
+    doc.autoformat();
+    Ok(doc.to_string())
+}
+
+/// Serialize a value to a [`KdlDocument`].
+pub fn to_doc<T: Serialize>(value: &T) -> Result<KdlDocument> {
+    let v = value.serialize(ValueSerializer)?;
+    value_to_doc(v)
+}
+
+// ---------------------------------------------------------------------------
+// Intermediate value representation
+// ---------------------------------------------------------------------------
+
+/// An intermediate representation that bridges serde's data model and KDL's
+/// tree structure. We serialize into this first, then convert to KDL.
+#[derive(Debug, Clone)]
+enum Value {
+    Null,
+    Bool(bool),
+    Integer(i128),
+    Float(f64),
+    String(String),
+    Seq(Vec<Value>),
+    Map(Vec<(String, Value)>),
+}
+
+impl Value {
+    fn is_primitive(&self) -> bool {
+        matches!(
+            self,
+            Value::Null | Value::Bool(_) | Value::Integer(_) | Value::Float(_) | Value::String(_)
+        )
+    }
+
+    fn to_kdl_value(&self) -> Option<KdlValue> {
+        match self {
+            Value::Null => Some(KdlValue::Null),
+            Value::Bool(b) => Some(KdlValue::Bool(*b)),
+            Value::Integer(i) => Some(KdlValue::Integer(*i)),
+            Value::Float(f) => Some(KdlValue::Float(*f)),
+            Value::String(s) => Some(KdlValue::String(s.clone())),
+            _ => None,
+        }
+    }
+}
+
+// ---------------------------------------------------------------------------
+// Value → KDL conversion
+// ---------------------------------------------------------------------------
+
+fn value_to_doc(value: Value) -> Result<KdlDocument> {
+    match value {
+        Value::Map(entries) => {
+            let mut doc = KdlDocument::new();
+            for (key, val) in entries {
+                let nodes = value_to_nodes(&key, val)?;
+                for node in nodes {
+                    doc.nodes_mut().push(node);
+                }
+            }
+            Ok(doc)
+        }
+        _ => Err(Error::TopLevelNotStruct),
+    }
+}
+
+/// Convert a key-value pair to one or more KDL nodes.
+fn value_to_nodes(name: &str, value: Value) -> Result<Vec<KdlNode>> {
+    match value {
+        // Primitives → single node with one argument
+        Value::Null => {
+            let mut node = KdlNode::new(name);
+            node.push(KdlEntry::new(KdlValue::Null));
+            Ok(vec![node])
+        }
+        Value::Bool(b) => {
+            let mut node = KdlNode::new(name);
+            node.push(KdlEntry::new(KdlValue::Bool(b)));
+            Ok(vec![node])
+        }
+        Value::Integer(i) => {
+            let mut node = KdlNode::new(name);
+            node.push(KdlEntry::new(KdlValue::Integer(i)));
+            Ok(vec![node])
+        }
+        Value::Float(f) => {
+            let mut node = KdlNode::new(name);
+            node.push(KdlEntry::new(KdlValue::Float(f)));
+            Ok(vec![node])
+        }
+        Value::String(s) => {
+            let mut node = KdlNode::new(name);
+            node.push(KdlEntry::new(KdlValue::String(s)));
+            Ok(vec![node])
+        }
+
+        // Seq: if all elements are primitive → one node with multiple args
+        //      if elements are maps → multiple nodes with same name
+        //      mixed → use `-` children
+        Value::Seq(elements) => {
+            if elements.is_empty() {
+                // Empty sequence → node with children block but no children
+                let mut node = KdlNode::new(name);
+                node.set_children(KdlDocument::new());
+                return Ok(vec![node]);
+            }
+
+            let all_primitive = elements.iter().all(|e| e.is_primitive());
+            let all_maps = elements.iter().all(|e| matches!(e, Value::Map(_)));
+
+            if all_primitive {
+                // Single node, multiple arguments
+                let mut node = KdlNode::new(name);
+                for elem in &elements {
+                    if let Some(kv) = elem.to_kdl_value() {
+                        node.push(KdlEntry::new(kv));
+                    }
+                }
+                Ok(vec![node])
+            } else if all_maps {
+                // Multiple nodes with the same name
+                let mut nodes = Vec::new();
+                for elem in elements {
+                    if let Value::Map(entries) = elem {
+                        let mut node = KdlNode::new(name);
+                        let children_doc = map_entries_to_doc(entries)?;
+                        node.set_children(children_doc);
+                        nodes.push(node);
+                    }
+                }
+                Ok(nodes)
+            } else {
+                // Mixed or nested sequences → use `-` children
+                let mut node = KdlNode::new(name);
+                let mut children = KdlDocument::new();
+                for elem in elements {
+                    let child_nodes = value_to_nodes("-", elem)?;
+                    for cn in child_nodes {
+                        children.nodes_mut().push(cn);
+                    }
+                }
+                node.set_children(children);
+                Ok(vec![node])
+            }
+        }
+
+        // Map → node with children
+        Value::Map(entries) => {
+            let mut node = KdlNode::new(name);
+            let children_doc = map_entries_to_doc(entries)?;
+            node.set_children(children_doc);
+            Ok(vec![node])
+        }
+    }
+}
+
+fn map_entries_to_doc(entries: Vec<(String, Value)>) -> Result<KdlDocument> {
+    let mut doc = KdlDocument::new();
+    for (k, v) in entries {
+        let nodes = value_to_nodes(&k, v)?;
+        for node in nodes {
+            doc.nodes_mut().push(node);
+        }
+    }
+    Ok(doc)
+}
+
+// ---------------------------------------------------------------------------
+// ValueSerializer: serde Serializer that produces a Value
+// ---------------------------------------------------------------------------
+
+struct ValueSerializer;
+
+impl ser::Serializer for ValueSerializer {
+    type Ok = Value;
+    type Error = Error;
+
+    type SerializeSeq = SerializeSeq;
+    type SerializeTuple = SerializeSeq;
+    type SerializeTupleStruct = SerializeSeq;
+    type SerializeTupleVariant = SerializeTupleVariant;
+    type SerializeMap = SerializeMap;
+    type SerializeStruct = SerializeMap;
+    type SerializeStructVariant = SerializeStructVariant;
+
+    fn serialize_bool(self, v: bool) -> Result<Value> {
+        Ok(Value::Bool(v))
+    }
+
+    fn serialize_i8(self, v: i8) -> Result<Value> {
+        Ok(Value::Integer(v as i128))
+    }
+
+    fn serialize_i16(self, v: i16) -> Result<Value> {
+        Ok(Value::Integer(v as i128))
+    }
+
+    fn serialize_i32(self, v: i32) -> Result<Value> {
+        Ok(Value::Integer(v as i128))
+    }
+
+    fn serialize_i64(self, v: i64) -> Result<Value> {
+        Ok(Value::Integer(v as i128))
+    }
+
+    fn serialize_i128(self, v: i128) -> Result<Value> {
+        Ok(Value::Integer(v))
+    }
+
+    fn serialize_u8(self, v: u8) -> Result<Value> {
+        Ok(Value::Integer(v as i128))
+    }
+
+    fn serialize_u16(self, v: u16) -> Result<Value> {
+        Ok(Value::Integer(v as i128))
+    }
+
+    fn serialize_u32(self, v: u32) -> Result<Value> {
+        Ok(Value::Integer(v as i128))
+    }
+
+    fn serialize_u64(self, v: u64) -> Result<Value> {
+        Ok(Value::Integer(v as i128))
+    }
+
+    fn serialize_u128(self, v: u128) -> Result<Value> {
+        Ok(Value::Integer(v as i128))
+    }
+
+    fn serialize_f32(self, v: f32) -> Result<Value> {
+        Ok(Value::Float(v as f64))
+    }
+
+    fn serialize_f64(self, v: f64) -> Result<Value> {
+        Ok(Value::Float(v))
+    }
+
+    fn serialize_char(self, v: char) -> Result<Value> {
+        Ok(Value::String(v.to_string()))
+    }
+
+    fn serialize_str(self, v: &str) -> Result<Value> {
+        Ok(Value::String(v.to_string()))
+    }
+
+    fn serialize_bytes(self, v: &[u8]) -> Result<Value> {
+        Ok(Value::Seq(
+            v.iter().map(|b| Value::Integer(*b as i128)).collect(),
+        ))
+    }
+
+    fn serialize_none(self) -> Result<Value> {
+        Ok(Value::Null)
+    }
+
+    fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<Value> {
+        value.serialize(self)
+    }
+
+    fn serialize_unit(self) -> Result<Value> {
+        Ok(Value::Null)
+    }
+
+    fn serialize_unit_struct(self, _name: &'static str) -> Result<Value> {
+        Ok(Value::Null)
+    }
+
+    fn serialize_unit_variant(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        variant: &'static str,
+    ) -> Result<Value> {
+        Ok(Value::String(variant.to_string()))
+    }
+
+    fn serialize_newtype_struct<T: ?Sized + Serialize>(
+        self,
+        _name: &'static str,
+        value: &T,
+    ) -> Result<Value> {
+        value.serialize(self)
+    }
+
+    fn serialize_newtype_variant<T: ?Sized + Serialize>(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        variant: &'static str,
+        value: &T,
+    ) -> Result<Value> {
+        let inner = value.serialize(ValueSerializer)?;
+        Ok(Value::Map(vec![(variant.to_string(), inner)]))
+    }
+
+    fn serialize_seq(self, len: Option<usize>) -> Result<SerializeSeq> {
+        Ok(SerializeSeq {
+            elements: Vec::with_capacity(len.unwrap_or(0)),
+        })
+    }
+
+    fn serialize_tuple(self, len: usize) -> Result<SerializeSeq> {
+        self.serialize_seq(Some(len))
+    }
+
+    fn serialize_tuple_struct(
+        self,
+        _name: &'static str,
+        len: usize,
+    ) -> Result<SerializeSeq> {
+        self.serialize_seq(Some(len))
+    }
+
+    fn serialize_tuple_variant(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        variant: &'static str,
+        len: usize,
+    ) -> Result<SerializeTupleVariant> {
+        Ok(SerializeTupleVariant {
+            variant: variant.to_string(),
+            elements: Vec::with_capacity(len),
+        })
+    }
+
+    fn serialize_map(self, len: Option<usize>) -> Result<SerializeMap> {
+        Ok(SerializeMap {
+            entries: Vec::with_capacity(len.unwrap_or(0)),
+            current_key: None,
+        })
+    }
+
+    fn serialize_struct(self, _name: &'static str, len: usize) -> Result<SerializeMap> {
+        self.serialize_map(Some(len))
+    }
+
+    fn serialize_struct_variant(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        variant: &'static str,
+        len: usize,
+    ) -> Result<SerializeStructVariant> {
+        Ok(SerializeStructVariant {
+            variant: variant.to_string(),
+            entries: Vec::with_capacity(len),
+        })
+    }
+}
+
+// ---------------------------------------------------------------------------
+// Compound serializer types
+// ---------------------------------------------------------------------------
+
+struct SerializeSeq {
+    elements: Vec<Value>,
+}
+
+impl ser::SerializeSeq for SerializeSeq {
+    type Ok = Value;
+    type Error = Error;
+
+    fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
+        self.elements.push(value.serialize(ValueSerializer)?);
+        Ok(())
+    }
+
+    fn end(self) -> Result<Value> {
+        Ok(Value::Seq(self.elements))
+    }
+}
+
+impl ser::SerializeTuple for SerializeSeq {
+    type Ok = Value;
+    type Error = Error;
+
+    fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
+        ser::SerializeSeq::serialize_element(self, value)
+    }
+
+    fn end(self) -> Result<Value> {
+        ser::SerializeSeq::end(self)
+    }
+}
+
+impl ser::SerializeTupleStruct for SerializeSeq {
+    type Ok = Value;
+    type Error = Error;
+
+    fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
+        ser::SerializeSeq::serialize_element(self, value)
+    }
+
+    fn end(self) -> Result<Value> {
+        ser::SerializeSeq::end(self)
+    }
+}
+
+struct SerializeTupleVariant {
+    variant: String,
+    elements: Vec<Value>,
+}
+
+impl ser::SerializeTupleVariant for SerializeTupleVariant {
+    type Ok = Value;
+    type Error = Error;
+
+    fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
+        self.elements.push(value.serialize(ValueSerializer)?);
+        Ok(())
+    }
+
+    fn end(self) -> Result<Value> {
+        Ok(Value::Map(vec![(
+            self.variant,
+            Value::Seq(self.elements),
+        )]))
+    }
+}
+
+struct SerializeMap {
+    entries: Vec<(String, Value)>,
+    current_key: Option<String>,
+}
+
+impl ser::SerializeMap for SerializeMap {
+    type Ok = Value;
+    type Error = Error;
+
+    fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<()> {
+        let key_val = key.serialize(ValueSerializer)?;
+        let key_str = match key_val {
+            Value::String(s) => s,
+            Value::Integer(i) => i.to_string(),
+            Value::Bool(b) => b.to_string(),
+            other => {
+                return Err(Error::Unsupported(format!(
+                    "map key must be a string, got {other:?}"
+                )))
+            }
+        };
+        self.current_key = Some(key_str);
+        Ok(())
+    }
+
+    fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
+        let key = self
+            .current_key
+            .take()
+            .ok_or_else(|| Error::Message("serialize_value called before serialize_key".into()))?;
+        let val = value.serialize(ValueSerializer)?;
+        // Skip None/Null values (they represent absent optional fields)
+        if !matches!(val, Value::Null) {
+            self.entries.push((key, val));
+        }
+        Ok(())
+    }
+
+    fn end(self) -> Result<Value> {
+        Ok(Value::Map(self.entries))
+    }
+}
+
+impl ser::SerializeStruct for SerializeMap {
+    type Ok = Value;
+    type Error = Error;
+
+    fn serialize_field<T: ?Sized + Serialize>(
+        &mut self,
+        key: &'static str,
+        value: &T,
+    ) -> Result<()> {
+        let val = value.serialize(ValueSerializer)?;
+        // Skip None/Null values for struct fields (Option::None)
+        if !matches!(val, Value::Null) {
+            self.entries.push((key.to_string(), val));
+        }
+        Ok(())
+    }
+
+    fn end(self) -> Result<Value> {
+        Ok(Value::Map(self.entries))
+    }
+}
+
+struct SerializeStructVariant {
+    variant: String,
+    entries: Vec<(String, Value)>,
+}
+
+impl ser::SerializeStructVariant for SerializeStructVariant {
+    type Ok = Value;
+    type Error = Error;
+
+    fn serialize_field<T: ?Sized + Serialize>(
+        &mut self,
+        key: &'static str,
+        value: &T,
+    ) -> Result<()> {
+        let val = value.serialize(ValueSerializer)?;
+        if !matches!(val, Value::Null) {
+            self.entries.push((key.to_string(), val));
+        }
+        Ok(())
+    }
+
+    fn end(self) -> Result<Value> {
+        Ok(Value::Map(vec![(
+            self.variant,
+            Value::Map(self.entries),
+        )]))
+    }
+}
diff --git a/tests/integration.rs b/tests/integration.rs
new file mode 100644
index 0000000..323e536
--- /dev/null
+++ b/tests/integration.rs
@@ -0,0 +1,579 @@
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+
+// ── Basic struct ───────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct SimpleConfig {
+    title: String,
+    count: i32,
+    enabled: bool,
+    ratio: f64,
+}
+
+#[test]
+fn deserialize_simple_struct() {
+    let input = r#"
+title "My App"
+count 42
+enabled #true
+ratio 3.14
+"#;
+    let config: SimpleConfig = serde_kdl::from_str(input).unwrap();
+    assert_eq!(config.title, "My App");
+    assert_eq!(config.count, 42);
+    assert_eq!(config.enabled, true);
+    assert_eq!(config.ratio, 3.14);
+}
+
+#[test]
+fn serialize_simple_struct() {
+    let config = SimpleConfig {
+        title: "My App".into(),
+        count: 42,
+        enabled: true,
+        ratio: 3.14,
+    };
+    let output = serde_kdl::to_string(&config).unwrap();
+    let roundtrip: SimpleConfig = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(config, roundtrip);
+}
+
+// ── Nested struct ──────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Server {
+    host: String,
+    port: u16,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct AppConfig {
+    name: String,
+    server: Server,
+}
+
+#[test]
+fn deserialize_nested_struct() {
+    let input = r#"
+name "webapp"
+server {
+    host "localhost"
+    port 8080
+}
+"#;
+    let config: AppConfig = serde_kdl::from_str(input).unwrap();
+    assert_eq!(config.name, "webapp");
+    assert_eq!(config.server.host, "localhost");
+    assert_eq!(config.server.port, 8080);
+}
+
+#[test]
+fn serialize_nested_struct() {
+    let config = AppConfig {
+        name: "webapp".into(),
+        server: Server {
+            host: "localhost".into(),
+            port: 8080,
+        },
+    };
+    let output = serde_kdl::to_string(&config).unwrap();
+    let roundtrip: AppConfig = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(config, roundtrip);
+}
+
+// ── Vec of primitives ──────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Tagged {
+    name: String,
+    tags: Vec<String>,
+}
+
+#[test]
+fn deserialize_vec_primitives() {
+    let input = r#"
+name "project"
+tags "web" "rust" "config"
+"#;
+    let tagged: Tagged = serde_kdl::from_str(input).unwrap();
+    assert_eq!(tagged.name, "project");
+    assert_eq!(tagged.tags, vec!["web", "rust", "config"]);
+}
+
+#[test]
+fn serialize_vec_primitives() {
+    let tagged = Tagged {
+        name: "project".into(),
+        tags: vec!["web".into(), "rust".into(), "config".into()],
+    };
+    let output = serde_kdl::to_string(&tagged).unwrap();
+    let roundtrip: Tagged = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(tagged, roundtrip);
+}
+
+// ── Vec of structs (repeated nodes) ────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Cluster {
+    server: Vec<Server>,
+}
+
+#[test]
+fn deserialize_vec_structs() {
+    let input = r#"
+server {
+    host "localhost"
+    port 8080
+}
+server {
+    host "example.com"
+    port 443
+}
+"#;
+    let cluster: Cluster = serde_kdl::from_str(input).unwrap();
+    assert_eq!(cluster.server.len(), 2);
+    assert_eq!(cluster.server[0].host, "localhost");
+    assert_eq!(cluster.server[0].port, 8080);
+    assert_eq!(cluster.server[1].host, "example.com");
+    assert_eq!(cluster.server[1].port, 443);
+}
+
+#[test]
+fn serialize_vec_structs() {
+    let cluster = Cluster {
+        server: vec![
+            Server {
+                host: "localhost".into(),
+                port: 8080,
+            },
+            Server {
+                host: "example.com".into(),
+                port: 443,
+            },
+        ],
+    };
+    let output = serde_kdl::to_string(&cluster).unwrap();
+    let roundtrip: Cluster = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(cluster, roundtrip);
+}
+
+// ── Dash children convention ───────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct DashList {
+    items: Vec<i32>,
+}
+
+#[test]
+fn deserialize_dash_children() {
+    let input = r#"
+items {
+    - 1
+    - 2
+    - 3
+}
+"#;
+    let list: DashList = serde_kdl::from_str(input).unwrap();
+    assert_eq!(list.items, vec![1, 2, 3]);
+}
+
+// ── Option fields ──────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct OptionalFields {
+    required: String,
+    optional: Option<String>,
+}
+
+#[test]
+fn deserialize_option_present() {
+    let input = r#"
+required "hello"
+optional "world"
+"#;
+    let val: OptionalFields = serde_kdl::from_str(input).unwrap();
+    assert_eq!(val.required, "hello");
+    assert_eq!(val.optional, Some("world".into()));
+}
+
+#[test]
+fn deserialize_option_absent() {
+    let input = r#"
+required "hello"
+"#;
+    let val: OptionalFields = serde_kdl::from_str(input).unwrap();
+    assert_eq!(val.required, "hello");
+    assert_eq!(val.optional, None);
+}
+
+#[test]
+fn deserialize_option_null() {
+    let input = r#"
+required "hello"
+optional #null
+"#;
+    let val: OptionalFields = serde_kdl::from_str(input).unwrap();
+    assert_eq!(val.required, "hello");
+    assert_eq!(val.optional, None);
+}
+
+#[test]
+fn serialize_option() {
+    let with = OptionalFields {
+        required: "hello".into(),
+        optional: Some("world".into()),
+    };
+    let output = serde_kdl::to_string(&with).unwrap();
+    assert!(output.contains("optional"));
+    let roundtrip: OptionalFields = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(with, roundtrip);
+
+    let without = OptionalFields {
+        required: "hello".into(),
+        optional: None,
+    };
+    let output = serde_kdl::to_string(&without).unwrap();
+    // None fields should be omitted
+    assert!(!output.contains("optional"));
+    let roundtrip: OptionalFields = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(without, roundtrip);
+}
+
+// ── Enum variants ──────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+enum Color {
+    Red,
+    Green,
+    Blue,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Colored {
+    name: String,
+    color: Color,
+}
+
+#[test]
+fn deserialize_unit_variant() {
+    let input = r#"
+name "widget"
+color "Red"
+"#;
+    let val: Colored = serde_kdl::from_str(input).unwrap();
+    assert_eq!(val.color, Color::Red);
+}
+
+#[test]
+fn serialize_unit_variant() {
+    let val = Colored {
+        name: "widget".into(),
+        color: Color::Green,
+    };
+    let output = serde_kdl::to_string(&val).unwrap();
+    let roundtrip: Colored = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(val, roundtrip);
+}
+
+// ── Struct variant enum ────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+enum Shape {
+    Circle { radius: f64 },
+    Rectangle { width: f64, height: f64 },
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Drawing {
+    name: String,
+    shape: Shape,
+}
+
+#[test]
+fn deserialize_struct_variant() {
+    let input = r#"
+name "my drawing"
+shape {
+    Circle {
+        radius 5.0
+    }
+}
+"#;
+    let val: Drawing = serde_kdl::from_str(input).unwrap();
+    assert_eq!(val.name, "my drawing");
+    assert_eq!(val.shape, Shape::Circle { radius: 5.0 });
+}
+
+#[test]
+fn serialize_struct_variant() {
+    let val = Drawing {
+        name: "my drawing".into(),
+        shape: Shape::Rectangle {
+            width: 10.0,
+            height: 20.0,
+        },
+    };
+    let output = serde_kdl::to_string(&val).unwrap();
+    let roundtrip: Drawing = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(val, roundtrip);
+}
+
+// ── Newtype variant enum ───────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+enum Wrapper {
+    Text(String),
+    Number(i64),
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Wrapped {
+    value: Wrapper,
+}
+
+#[test]
+fn deserialize_newtype_variant() {
+    let input = r#"
+value {
+    Text "hello"
+}
+"#;
+    let val: Wrapped = serde_kdl::from_str(input).unwrap();
+    assert_eq!(val.value, Wrapper::Text("hello".into()));
+}
+
+#[test]
+fn serialize_newtype_variant() {
+    let val = Wrapped {
+        value: Wrapper::Text("hello".into()),
+    };
+    let output = serde_kdl::to_string(&val).unwrap();
+    let roundtrip: Wrapped = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(val, roundtrip);
+}
+
+// ── HashMap ────────────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct WithMap {
+    settings: HashMap<String, String>,
+}
+
+#[test]
+fn deserialize_hashmap() {
+    let input = r#"
+settings {
+    key1 "value1"
+    key2 "value2"
+}
+"#;
+    let val: WithMap = serde_kdl::from_str(input).unwrap();
+    assert_eq!(val.settings.get("key1"), Some(&"value1".into()));
+    assert_eq!(val.settings.get("key2"), Some(&"value2".into()));
+}
+
+#[test]
+fn serialize_hashmap() {
+    let mut settings = HashMap::new();
+    settings.insert("key1".into(), "value1".into());
+    let val = WithMap { settings };
+    let output = serde_kdl::to_string(&val).unwrap();
+    let roundtrip: WithMap = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(val, roundtrip);
+}
+
+// ── Various integer types ──────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct IntTypes {
+    a: u8,
+    b: u16,
+    c: u32,
+    d: u64,
+    e: i8,
+    f: i16,
+    g: i32,
+    h: i64,
+}
+
+#[test]
+fn roundtrip_int_types() {
+    let val = IntTypes {
+        a: 1,
+        b: 2,
+        c: 3,
+        d: 4,
+        e: -1,
+        f: -2,
+        g: -3,
+        h: -4,
+    };
+    let output = serde_kdl::to_string(&val).unwrap();
+    let roundtrip: IntTypes = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(val, roundtrip);
+}
+
+// ── Tuple ──────────────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct WithTuple {
+    point: (f64, f64, f64),
+}
+
+#[test]
+fn deserialize_tuple() {
+    let input = r#"
+point 1.0 2.0 3.0
+"#;
+    let val: WithTuple = serde_kdl::from_str(input).unwrap();
+    assert_eq!(val.point, (1.0, 2.0, 3.0));
+}
+
+#[test]
+fn roundtrip_tuple() {
+    let val = WithTuple {
+        point: (1.0, 2.0, 3.0),
+    };
+    let output = serde_kdl::to_string(&val).unwrap();
+    let roundtrip: WithTuple = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(val, roundtrip);
+}
+
+// ── Deeply nested ──────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Level3 {
+    value: String,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Level2 {
+    inner: Level3,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Level1 {
+    middle: Level2,
+}
+
+#[test]
+fn deeply_nested() {
+    let input = r#"
+middle {
+    inner {
+        value "deep"
+    }
+}
+"#;
+    let val: Level1 = serde_kdl::from_str(input).unwrap();
+    assert_eq!(val.middle.inner.value, "deep");
+
+    let output = serde_kdl::to_string(&val).unwrap();
+    let roundtrip: Level1 = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(val, roundtrip);
+}
+
+// ── Boolean values ─────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Booleans {
+    yes: bool,
+    no: bool,
+}
+
+#[test]
+fn booleans() {
+    let input = r#"
+yes #true
+no #false
+"#;
+    let val: Booleans = serde_kdl::from_str(input).unwrap();
+    assert_eq!(val.yes, true);
+    assert_eq!(val.no, false);
+
+    let output = serde_kdl::to_string(&val).unwrap();
+    let roundtrip: Booleans = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(val, roundtrip);
+}
+
+// ── Empty vec ──────────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct WithVec {
+    items: Vec<String>,
+}
+
+#[test]
+fn serialize_empty_vec() {
+    let val = WithVec { items: vec![] };
+    let output = serde_kdl::to_string(&val).unwrap();
+    // Empty vec gets an empty children block
+    let roundtrip: WithVec = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(roundtrip.items, Vec::<String>::new());
+}
+
+// ── to_string_pretty ───────────────────────────────────────────────────
+
+#[test]
+fn pretty_print() {
+    let config = AppConfig {
+        name: "webapp".into(),
+        server: Server {
+            host: "localhost".into(),
+            port: 8080,
+        },
+    };
+    let pretty = serde_kdl::to_string_pretty(&config).unwrap();
+    // Should still roundtrip
+    let roundtrip: AppConfig = serde_kdl::from_str(&pretty).unwrap();
+    assert_eq!(config, roundtrip);
+}
+
+// ── to_doc / from_doc ──────────────────────────────────────────────────
+
+#[test]
+fn doc_api() {
+    let config = SimpleConfig {
+        title: "Test".into(),
+        count: 1,
+        enabled: false,
+        ratio: 0.5,
+    };
+    let doc = serde_kdl::to_doc(&config).unwrap();
+    assert!(doc.get("title").is_some());
+    let roundtrip: SimpleConfig = serde_kdl::from_doc(&doc).unwrap();
+    assert_eq!(config, roundtrip);
+}
+
+// ── Vec of integers ────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Numbers {
+    values: Vec<i32>,
+}
+
+#[test]
+fn roundtrip_vec_ints() {
+    let val = Numbers {
+        values: vec![1, 2, 3, 4, 5],
+    };
+    let output = serde_kdl::to_string(&val).unwrap();
+    let roundtrip: Numbers = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(val, roundtrip);
+}
+
+// ── Char field ─────────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct WithChar {
+    letter: char,
+}
+
+#[test]
+fn roundtrip_char() {
+    let val = WithChar { letter: 'X' };
+    let output = serde_kdl::to_string(&val).unwrap();
+    let roundtrip: WithChar = serde_kdl::from_str(&output).unwrap();
+    assert_eq!(val, roundtrip);
+}