facet-kdl 0.32.2

KDL serialization and deserialization for Facet types
Documentation

facet-kdl

Coverage Status crates.io documentation MIT/Apache-2.0 licensed Discord

facet-kdl

KDL serialization and deserialization for Facet types.

Quick start

Add facet-kdl alongside your Facet types and derive Facet:

use facet::Facet;
use facet_kdl as kdl;

#[derive(Facet, Debug, PartialEq)]
struct Config {
    #[facet(kdl::child)]
    server: Server,
}

#[derive(Facet, Debug, PartialEq)]
struct Server {
    #[facet(kdl::argument)]
    host: String,
    #[facet(kdl::property)]
    port: u16,
}

fn main() -> Result<(), facet_kdl::KdlError> {
    let cfg: Config = facet_kdl::from_str(r#"server "localhost" port=8080"#)?;
    assert_eq!(cfg.server.port, 8080);

    let text = facet_kdl::to_string(&cfg)?;
    assert_eq!(text, "server \"localhost\" port=8080\n");
    Ok(())
}

Common patterns

  • #[facet(kdl::child)] for a single required child node, #[facet(kdl::children)] for lists/maps/sets of children.
  • #[facet(kdl::property)] maps node properties (key/value pairs) to fields.
  • #[facet(kdl::arguments)]/#[facet(kdl::argument)] read positional arguments on a node.
  • #[facet(flatten)] merges nested structs/enums; the solver uses property/child presence to choose variants.
  • Spanned<T> is supported: properties/arguments can be captured with miette::SourceSpan data.

Multiple children fields

When a struct has a single #[facet(kdl::children)] field, all child nodes are collected into that field.

When a struct has multiple #[facet(kdl::children)] fields, nodes are routed based on matching the node name to the singular form of the field name:

use facet::Facet;
use facet_kdl as kdl;

#[derive(Facet, Debug)]
struct Config {
    #[facet(kdl::children, default)]
    dependencies: Vec<Dependency>,

    #[facet(kdl::children, default)]
    samples: Vec<Sample>,
}

#[derive(Facet, Debug)]
struct Dependency {
    #[facet(kdl::argument)]
    name: String,
    #[facet(kdl::property)]
    version: String,
}

#[derive(Facet, Debug)]
struct Sample {
    #[facet(kdl::argument)]
    path: String,
}

With this KDL:

dependency "serde" version="1.0"
sample "test.txt"
dependency "tokio" version="1.0"
sample "example.txt"

The nodes are routed based on name matching:

  • dependency nodes → dependencies field (singular matches plural)
  • sample nodes → samples field

Supported pluralization patterns:

  • Simple s: itemitems
  • ies ending: dependencydependencies
  • es ending: boxboxes

Note: Use #[facet(default)] on children fields to allow them to be empty when no matching nodes are present.

KDL syntax: arguments vs properties

A common source of confusion is the difference between arguments and properties in KDL:

// Arguments are positional values after the node name
server "localhost" 8080

// Properties are key=value pairs
server host="localhost" port=8080

// You can mix both - arguments come first, then properties
server "localhost" port=8080

This matters for your struct definitions:

use facet::Facet;
use facet_kdl as kdl;

// For: server "localhost" port=8080
#[derive(Facet)]
struct Server {
    #[facet(kdl::argument)]  // captures "localhost"
    host: String,
    #[facet(kdl::property)]  // captures port=8080
    port: u16,
}

Child nodes with arguments

A particularly common KDL pattern uses child nodes with arguments:

config {
    name "my-app"
    version "1.0.0"
    debug true
}

Here, name "my-app" is a child node named name with an argument "my-app". This is not a property (which would be name="my-app").

To deserialize this pattern, each child node needs its own struct with a kdl::argument field:

use facet::Facet;
use facet_kdl as kdl;

#[derive(Facet)]
struct Config {
    #[facet(kdl::child)]
    name: Name,
    #[facet(kdl::child)]
    version: Version,
    #[facet(kdl::child)]
    debug: Debug,
}

#[derive(Facet)]
struct Name {
    #[facet(kdl::argument)]
    value: String,
}

#[derive(Facet)]
struct Version {
    #[facet(kdl::argument)]
    value: String,
}

#[derive(Facet)]
struct Debug {
    #[facet(kdl::argument)]
    value: bool,
}

If you're getting "no matching argument field for value" errors, check whether your KDL uses name "value" (child node with argument) vs name="value" (property) syntax.

Feature flags

  • default/std: enables std for dependencies.
  • alloc: no_std builds with alloc only.

Error reporting

Errors use miette spans where possible, so diagnostics can point back to the offending KDL source.

License

MIT OR Apache-2.0, at your option.

Sponsors

Thanks to all individual sponsors:

...along with corporate sponsors:

...without whom this work could not exist.

Special thanks

The facet logo was drawn by Misiasart.

License

Licensed under either of:

at your option.