Architecture
confval turns a configuration file into runtime types using a compiler-like architecture.
See The pipeline contract for more architecture details.
One spec, any format
Write the same configuration in TOML, HCL, KDL, JSON, or YAML. Each parses into the identical runtime type, so validation and lowering never depend on which format an operator chose.
- TOML
- HCL
- KDL
- JSON
- YAML
hostname = "127.0.0.1"
port = 8080
allow = ["10.0.0.0/8", "192.168.0.0/16"]
[bind]
port = 8080
hostname = "127.0.0.1"
port = 8080
allow = ["10.0.0.0/8", "192.168.0.0/16"]
bind {
port = 8080
}
hostname "127.0.0.1"
port 8080
allow "10.0.0.0/8" "192.168.0.0/16"
bind {
port 8080
}
{
"hostname": "127.0.0.1",
"port": 8080,
"allow": ["10.0.0.0/8", "192.168.0.0/16"],
"bind": { "port": 8080 }
}
hostname: "127.0.0.1"
port: 8080
allow: ["10.0.0.0/8", "192.168.0.0/16"]
bind:
port: 8080
A documented starting point
confval generates an annotated template from your spec, with every setting, its doc comment, and each optional field commented out, so an operator starts from a complete file instead of guessing keys.
# The host the server binds to.
hostname = "127.0.0.1"
# The port to listen on.
port = 8080
# CIDR ranges allowed to connect.
allow = ["10.0.0.0/8"]
# Connection limits.
[limits]
# Largest request body, in megabytes.
max_body_mb = 16
# Enforcement mode, optional. Defaults to "enforce".
# mode = "enforce"
Diagnostics
confval produces operator-friendly, accumulated validation diagnostics in a variety of formats (i.e., pretty, plain, and JSON).
This invalid TOML config file...

Produces these diagnostics...

Strict parsing for LLM-edited configs
confval rejects any key the spec does not define, so an LLM that invents a setting gets a clear error rather than a silent misconfiguration. Every problem is reported at once, at its source location.
Config becomes real Rust types
A spec holds the widest form of each value, so parsing stays permissive. Lowering then narrows every field to its exact runtime type, and a value that does not fit is reported at its source span rather than silently truncated.
Spec
#[derive(confval::Spec)]
struct ServerSpec {
port: Located<i64>,
mode: Located<String>,
}
Config
#[derive(confval::Config)]
#[confval(lower_from = ServerSpec)]
struct ServerConfig {
#[confval(lower(from = port, with = narrow::i64_to_u16))]
port: u16,
#[confval(lower(from = mode, with = narrow::keyword::<Mode>))]
mode: Mode,
}
port narrows an i64 to a u16, and mode lowers a validated string into an enum.
Layered configuration
Assemble one config from layers. A file supplies the base, environment variables override it, and CLI flags override those. Every layer yields the same format-neutral model, so the merge runs once, before the spec is built.
Convert between formats
Every format parses into one format-neutral model, and every emitter writes that model back out. So confval reads one format and writes another, for the shapes the target format can represent, with no schema needed.
Read HCL
hostname = "127.0.0.1"
port = 8080
allow = ["10.0.0.0/8", "192.168.0.0/16"]
bind {
port = 8080
}
Write JSON
{
"hostname": "127.0.0.1",
"port": 8080,
"allow": ["10.0.0.0/8", "192.168.0.0/16"],
"bind": { "port": 8080 }
}
e.g., emit_json(&parse_hcl_fields(...)?)
