[!NOTE] This repository is archived. The
kdlcrate has built-in Serde support, on by default, since version 6.6.0. Use that instead.
serde-kdl2
Serde integration for KDL (KDL Document Language).
Built on top of kdl v6.5 (KDL v2 spec).
Quick Start
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_kdl2::from_str(kdl_input).unwrap();
// Serialize
let output = serde_kdl2::to_string(&config).unwrap();
// Roundtrip
let roundtrip: Config = serde_kdl2::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.
title "My App"
count 42
enabled #true
Nested Structs → Children Blocks
server {
host "localhost"
port 8080
}
Vec of Primitives → Multiple Arguments
tags "web" "rust" "config"
Vec of Structs → Repeated Nodes
server {
host "localhost"
port 8080
}
server {
host "example.com"
port 443
}
Dash Children Convention
For deserialization, the - (dash) node name convention is supported:
items {
- 1
- 2
- 3
}
Tuples → Multiple Arguments
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:
color "Red"
Newtype variants use the variant name as a child node name:
value {
Text "hello"
}
Struct variants use the variant name as a child node with a children block:
shape {
Circle {
radius 5.0
}
}
Tuple variants use the variant name as a child node with multiple arguments:
data {
Point 1.0 2.0 3.0
}
HashMap / BTreeMap
Maps serialize identically to structs — each key becomes a node name:
settings {
key1 "value1"
key2 "value2"
}
API
// Deserialize from string
let config: Config = serde_kdl2::from_str(kdl_str)?;
// Deserialize from KdlDocument
let config: Config = serde_kdl2::from_doc(&doc)?;
// Serialize to string
let s: String = serde_kdl2::to_string(&config)?;
// Serialize to string (auto-formatted)
let s: String = serde_kdl2::to_string_pretty(&config)?;
// Serialize to KdlDocument
let doc: kdl::KdlDocument = serde_kdl2::to_doc(&config)?;
Releasing
just release X.Y.Z
This bumps the version in Cargo.toml, commits, tags, and pushes. GitHub
Actions then publishes the crate to crates.io and creates a GitHub release.
Potential upstream issues
Property testing surfaced two roundtrip failures that appear to originate in
the kdl crate (v6.5). These need further investigation to confirm root
causes and determine whether to file upstream issues.
Control character escaping. Strings containing certain control characters
serialize to invalid KDL that the parser rejects. The kdl crate's Display
for string values escapes \\, ", \n, \r, \t, \b, and \f, but
appears to write other control characters verbatim. serde-kdl2 works around
this by setting a custom value_repr with \u{…} escapes on affected
entries. See string_entry() in ser.rs.
i128::MIN roundtrip failure. Serializing i128::MIN produces valid KDL
text, but the kdl parser rejects it. Possibly a sign-magnitude overflow during
parsing (the absolute value of i128::MIN exceeds i128::MAX), but this
hasn't been confirmed by reading the parser source. serde-kdl2 doesn't work
around this — callers should avoid i128::MIN if roundtripping matters.
AI Usage
I built this crate with substantial help from Claude (Anthropic). The AI
wrote most of the initial serializer, deserializer, and test code. I directed
the design, reviewed every change, and iterated on the mapping rules. Commit
messages note AI assistance with Assisted-by footers.
License
MIT