Skip to main content
confval logo

confval

BuildCoverageTests

A batteries-included configuration toolkit for Rust

Define a configuration as Rust types, parse a file into them, validate the values, and lower them into the types your program runs on. Errors report the line and column they came from.

The confval language server showing autocomplete, hover documentation, and live diagnostics in a configuration file

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.

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...

Example of pretty-formatted validation diagnostics for invalid TOML configuration

Produces these diagnostics...

Example of pretty-formatted validation diagnostics for invalid TOML configuration

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(...)?)