Expand description
§tier
tier is a Rust configuration library for typed, layered application config.
It is designed for projects that want one serde config type fed by code
defaults, TOML files, environment variables, and CLI overrides, without
falling back to untyped value trees.
By default, tier only enables TOML file support. derive, clap, schema,
watch, json, and yaml are opt-in features.
Use tier when you want:
- a Rust config library built around
serdetypes - predictable layered config from defaults, files, env, and CLI
- source tracing and validation instead of silent config drift
- optional schema, docs, and reload support without a heavy default feature set
§Feature Flags
toml: TOML file parsing and commented TOML examplesderive:#[derive(TierConfig)]metadata generationclap: reusable config flags and diagnostics commandsschema: JSON Schema, env docs, and machine-readable reportswatch: native filesystem watcher backendjson: JSON file parsingyaml: YAML file parsing
§Feature Map
- Loading:
ConfigLoader,FileSource,EnvSource,ArgsSource - Metadata:
ConfigMetadata,FieldMetadata,TierConfig - Diagnostics:
ConfigReport,doctor(),explain(),audit_report() - Schema and docs:
json_schema_*,annotated_json_schema_*,config_example_*,EnvDocOptions - Reload:
ReloadHandle,PollingWatcher,NativeWatcher
§Input Semantics
- Env values and
--set key=valueoverrides are string-first inputs - Primitive targets such as
bool, integers, floats, andOption<T>are coerced during deserialization - Use explicit JSON syntax for arrays, objects, or quoted strings when you need structured inline values
§Checked Paths
For refactor-heavy code, tier exposes checked path macros that still feed the
same runtime APIs:
use serde::{Deserialize, Serialize};
use tier::{ConfigMetadata, FieldMetadata};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AppConfig {
proxy: ProxyConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ProxyConfig {
no_proxy: Vec<String>,
}
let metadata = ConfigMetadata::from_fields([
FieldMetadata::new(tier::path!(AppConfig.proxy.no_proxy)),
]);
assert!(metadata.field("proxy.no_proxy").is_some());§Typed Patches
If your application already has typed override structs, derive TierPatch and
apply them directly instead of maintaining a serializable shadow layer.
use serde::{Deserialize, Serialize};
use tier::{ConfigLoader, Patch, TierPatch};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AppConfig {
port: u16,
token: Option<String>,
}
#[derive(Debug, Default, TierPatch)]
struct CliPatch {
port: Option<u16>,
#[tier(path_expr = tier::path!(AppConfig.token))]
token: Patch<Option<String>>,
}
let loaded = ConfigLoader::new(AppConfig {
port: 3000,
token: Some("default".to_owned()),
})
.patch(
"typed-cli",
&CliPatch {
port: Some(8080),
token: Patch::set(None),
},
)?
.load()?;
assert_eq!(loaded.port, 8080);
assert_eq!(loaded.token, None);With both derive and clap, the same typed CLI struct can be applied as the
last layer through ConfigLoader::clap_overrides(&parsed_cli), and
path_expr = tier::path!(...) keeps CLI-to-config mappings checked during
refactors.
§Structured Env Values
tier can decode common operational env formats without forcing applications
to pre-normalize them into JSON.
use serde::{Deserialize, Serialize};
use tier::{ConfigLoader, EnvDecoder, EnvSource};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct AppConfig {
no_proxy: Vec<String>,
}
let loaded = ConfigLoader::new(AppConfig { no_proxy: Vec::new() })
.env_decoder("no_proxy", EnvDecoder::Csv)
.env(EnvSource::from_pairs([(
"APP__NO_PROXY",
"localhost,127.0.0.1,.internal.example.com",
)]).prefix("APP"))
.load()?;
assert_eq!(
loaded.no_proxy,
vec![
"localhost".to_owned(),
"127.0.0.1".to_owned(),
".internal.example.com".to_owned(),
]
);You can also bridge standard operational env names directly:
use serde::{Deserialize, Serialize};
use tier::{ConfigLoader, EnvDecoder, EnvSource};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct AppConfig {
proxy: ProxyConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct ProxyConfig {
url: Option<String>,
no_proxy: Vec<String>,
}
let loaded = ConfigLoader::new(AppConfig::default())
.env(
EnvSource::from_pairs([
("HTTP_PROXY", "http://proxy.internal:8080"),
("NO_PROXY", "localhost,.svc.internal"),
])
.with_fallback("HTTP_PROXY", "proxy.url")
.with_fallback_decoder("NO_PROXY", "proxy.no_proxy", EnvDecoder::Csv),
)
.load()?;
assert_eq!(loaded.proxy.url.as_deref(), Some("http://proxy.internal:8080"));
assert_eq!(loaded.proxy.no_proxy, vec!["localhost", ".svc.internal"]);For formats that are not covered by the built-in EnvDecoder variants, use
ConfigLoader::env_decoder_with(...) to keep application-specific parsing in
the config layer instead of pre-normalizing env vars outside tier.
§Leaf Enums
Leaf enum fields can opt out of nested metadata requirements with
#[tier(leaf)] on the containing field:
use serde::{Deserialize, Serialize};
use tier::TierConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
enum Backend {
Memory,
Redis,
}
#[derive(Debug, Clone, Serialize, Deserialize, TierConfig)]
struct AppConfig {
#[tier(env = "APP_BACKEND", leaf)]
backend: Backend,
}§Quick Start
The smallest useful setup is defaults plus a TOML file:
use serde::{Deserialize, Serialize};
use tier::ConfigLoader;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AppConfig {
port: u16,
}
impl Default for AppConfig {
fn default() -> Self {
Self { port: 3000 }
}
}
let loaded = ConfigLoader::new(AppConfig::default())
.file("config/app.toml")
.load()?;
assert!(loaded.report().doctor().contains("Sources:"));§Examples
The crate ships with focused examples under examples/:
basic.rs: defaults + env + CLI layeringmanual-metadata.rs: explicitConfigMetadataderive.rs: derive metadata and declarative validationschema.rs: schema, env docs, and commented TOML examplesclap.rs: embeddingTierClireload.rs: polling reloadapplication.rs: a fuller application setup
§Core Types
ConfigLoader<T>builds a deterministic pipeline from defaults, files, env, CLI, and custom layers.ConfigMetadatacarries env names, aliases, secrets, examples, merge rules, and declared validations.LoadedConfig<T>returns the final typed value with aConfigReport.ReloadHandle<T>reuses the same loader closure for polling or native file watching.
§Highlights
- Typed loading with deterministic merge order and unknown field governance
- Metadata-driven env mapping, secret handling, and validation
- Field-level tracing, doctor output, and machine-readable audit/report data
- Optional schema/docs export, commented TOML examples,
clap, and reload
§Example
use serde::{Deserialize, Serialize};
use tier::{ArgsSource, ConfigLoader, EnvSource, Secret, TierConfig, ValidationErrors};
#[derive(Debug, Clone, Serialize, Deserialize, TierConfig)]
struct AppConfig {
server: ServerConfig,
db: DbConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, TierConfig)]
struct ServerConfig {
#[tier(
env = "APP_SERVER_HOST",
doc = "IP address or hostname to bind",
example = "0.0.0.0"
)]
host: String,
#[tier(deprecated = "use server.bind_port instead")]
port: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize, TierConfig)]
struct DbConfig {
password: Secret<String>,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
server: ServerConfig {
host: "127.0.0.1".into(),
port: 3000,
},
db: DbConfig {
password: Secret::new("secret".into()),
},
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let loaded = ConfigLoader::new(AppConfig::default())
.derive_metadata()
.file("config/default.toml")
.optional_file("config/{profile}.toml")
.env(EnvSource::prefixed("APP"))
.args(ArgsSource::from_env())
.profile("prod")
.validator("port-range", |config| {
if config.server.port == 0 {
return Err(ValidationErrors::from_message(
"server.port",
"port must be greater than zero",
));
}
Ok(())
})
.load()?;
println!("{}", loaded.report().doctor());
Ok(())
}derive_metadata() applies metadata generated by TierConfig, including env
names, aliases, secret handling, serde(default) awareness, merge strategies,
declared validation rules, env docs, and deprecation warnings.
§Declarative Validation
tier supports metadata-driven field and cross-field validation alongside
custom validator hooks. Declared rules feed the loader, schema annotations,
env docs, and commented TOML examples from the same metadata source.
use serde::{Deserialize, Serialize};
use tier::{ConfigError, ConfigLoader, TierConfig};
#[derive(Debug, Clone, Serialize, Deserialize, TierConfig)]
struct AppConfig {
#[tier(non_empty, min_length = 3)]
service_name: String,
#[tier(min = 1, max = 65535)]
port: u16,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
service_name: "api".to_owned(),
port: 8080,
}
}
}
let error = ConfigLoader::new(AppConfig::default())
.derive_metadata()
.args(tier::ArgsSource::from_args([
"app",
"--set",
r#"service_name="""#,
"--set",
"port=0",
]))
.load()
.expect_err("declared validation must fail");
assert!(matches!(error, ConfigError::DeclaredValidation { .. }));§Reload
tier always includes ReloadHandle and a polling watcher. With watch
enabled, it also exposes a native filesystem watcher. Reloads can emit
structured diffs and events, and watchers can either keep running or stop
after a failed reload.
use std::time::Duration;
use serde::{Deserialize, Serialize};
use tier::{ConfigError, ConfigLoader, ReloadEvent, ReloadHandle};
#[derive(Clone, Serialize, Deserialize)]
struct AppConfig {
port: u16,
}
impl Default for AppConfig {
fn default() -> Self {
Self { port: 3000 }
}
}
fn main() -> Result<(), ConfigError> {
let handle =
ReloadHandle::new(|| ConfigLoader::new(AppConfig::default()).file("app.toml").load())?;
let _events = handle.subscribe();
let _summary = handle.reload_detailed()?;
let watcher = handle.start_native(["app.toml"], Duration::from_millis(100))?;
watcher.stop();
Ok(())
}§Schema Export
With schema enabled, tier can export a JSON Schema for a config type:
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tier::json_schema_pretty;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct AppConfig {
port: u16,
}
let schema = json_schema_pretty::<AppConfig>();
assert!(schema.contains("\"type\": \"object\""));§Clap Integration
With clap enabled, tier provides a reusable config flag group:
use clap::Parser;
use tier::TierCli;
#[derive(Debug, Parser)]
struct AppCli {
#[command(flatten)]
config: TierCli,
}
let cli = AppCli::parse_from(["app", "--validate-config"]);
assert!(matches!(cli.config.command(), tier::TierCliCommand::ValidateConfig));
let example = AppCli::parse_from(["app", "--print-config-example"]);
assert!(matches!(
example.config.command(),
tier::TierCliCommand::PrintConfigExample
));§Environment Variable Docs
With schema enabled, tier can generate environment variable docs and
annotated JSON Schema. When toml is also enabled, it can render a commented
TOML example configuration:
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tier::{
EnvDocOptions, TierConfig, annotated_json_schema_pretty, config_example_toml, env_docs_json,
env_docs_markdown,
};
#[derive(Debug, Serialize, Deserialize, JsonSchema, TierConfig)]
struct AppConfig {
server: ServerConfig,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, TierConfig)]
struct ServerConfig {
#[tier(
env = "APP_SERVER_PORT",
doc = "Port used for incoming traffic",
example = "8080"
)]
port: u16,
}
let docs = env_docs_markdown::<AppConfig>(&EnvDocOptions::prefixed("APP"));
assert!(docs.contains("APP_SERVER_PORT"));
let docs_json = env_docs_json::<AppConfig>(&EnvDocOptions::prefixed("APP"));
assert!(docs_json.is_array());
let schema = annotated_json_schema_pretty::<AppConfig>();
assert!(schema.contains("\"x-tier-env\""));
let example = config_example_toml::<AppConfig>();
assert!(example.contains("[server]"));§Secrets
tier::Secret<T> is a strong typed wrapper for sensitive values. It redacts
Debug and Display output, and with the schema feature it marks fields as
writeOnly so the loader can auto-discover secret paths.
use serde::{Deserialize, Serialize};
use tier::Secret;
#[derive(Debug, Serialize, Deserialize)]
struct DbConfig {
password: Secret<String>,
}
let password = Secret::new("super-secret".to_owned());
assert_eq!(format!("{password}"), "***redacted***");Macros§
- path
- Builds a compile-time checked dot path from a config type.
- path_
pattern - Builds a compile-time checked wildcard path from a config type.
Structs§
- Args
Source - CLI override source definition.
- Audit
Report - Machine-readable audit payload including traces for every resolved path.
- Config
Change - A single redacted configuration change observed during reload.
- Config
Loader - Builder for layered configuration loading.
- Config
Metadata - Structured metadata describing configuration fields.
- Config
Report - Post-load diagnostics including source traces, warnings, and redacted output helpers.
- Deprecated
Field - Information about a deprecated configuration path used during loading.
- Doctor
Report - Machine-readable summary of a loaded configuration report.
- EnvDoc
Entry schema - A single schema-derived environment variable documentation row.
- EnvDoc
Options schema - Options controlling schema-derived environment variable documentation.
- EnvDocs
Report schema - Versioned machine-readable environment documentation payload.
- EnvSource
- Environment variable source definition.
- Explanation
- Full resolution trace for a single configuration path.
- Field
Metadata - Metadata for a single configuration path.
- File
Source - File-backed configuration source definition.
- Layer
- Custom serializable configuration layer.
- Loaded
Config - Loaded configuration plus its diagnostic report.
- Native
Watcher watch - Handle for a background native filesystem watcher.
- Polling
Watcher - Handle for a background polling watcher.
- Reload
Failure - Structured details about a rejected reload attempt.
- Reload
Handle - Thread-safe holder for the active configuration and reload logic.
- Reload
Options - Options controlling watcher-side reload behavior.
- Reload
Summary - Structured summary of a successful reload attempt.
- Report
Summary - Aggregate counts for a machine-readable configuration report.
- Resolution
Step - One source contribution recorded for a configuration path.
- Secret
- Strongly typed secret wrapper.
- Source
Trace - Human-readable description of where a configuration value came from.
- TierCli
clap - Reusable
clapflag group fortierconfiguration loading and diagnostics. - Trace
Audit - Structured audit details for a single resolved path.
- Unknown
Field - Information about an unknown configuration path discovered during loading.
- Validation
Error - A single validation failure returned by a validator hook.
- Validation
Errors - A collection of validation failures returned by a validator hook.
- Validation
Value - Scalar value used by declarative validation rules and conditions.
Enums§
- Config
Error - Errors returned while building, loading, validating, or inspecting configuration.
- Config
Warning - Non-fatal issues surfaced while loading configuration.
- EnvDecoder
- Built-in decoders for structured environment variable values.
- File
Format - Supported on-disk configuration file formats.
- Merge
Strategy - Strategy applied when multiple layers write to the same configuration path.
- Patch
- Sparse patch field wrapper used by typed override structs.
- Reload
Event - Structured event emitted after each successful or rejected reload attempt.
- Reload
Failure Policy - Policy applied when a background watcher encounters a reload failure.
- Source
Kind - Kind of source that contributed configuration values.
- Tier
CliCommand clap - High-level action requested by
TierCli. - Unknown
Field Policy - Policy applied when unknown configuration paths are discovered.
- Validation
Check - Cross-field declarative validation applied to the final normalized configuration.
- Validation
Number - Numeric bound used by declarative validation rules.
- Validation
Rule - Declarative validation rule applied to a single configuration path.
Constants§
- ENV_
DOCS_ FORMAT_ VERSION schema - Stable version tag for machine-readable environment documentation payloads.
- REPORT_
FORMAT_ VERSION - Stable version tag for machine-readable doctor and audit reports.
Traits§
- Json
Schema schema - Re-export of
schemars::JsonSchemaused bytierschema helpers. A type which can be described as a JSON Schema document. - Tier
Metadata - Metadata produced for a configuration type.
- Tier
Patch - Trait implemented by typed sparse override structures.
Functions§
- annotated_
json_ schema_ for schema - Exports the JSON Schema for a configuration type annotated with
tiermetadata. - annotated_
json_ schema_ pretty schema - Exports the annotated JSON Schema for a configuration type as pretty JSON.
- config_
example_ for schema - Generates a machine-readable example configuration value from schema and metadata.
- config_
example_ pretty schema - Renders the generated example configuration as pretty JSON.
- config_
example_ toml schemaandtoml - Renders the generated example configuration as commented TOML.
- env_
docs_ for schema - Generates environment variable documentation rows from a configuration schema.
- env_
docs_ json schema - Renders schema-derived environment variable documentation as machine-readable JSON.
- env_
docs_ json_ pretty schema - Renders machine-readable environment variable documentation as pretty JSON.
- env_
docs_ markdown schema - Renders schema-derived environment variable documentation as Markdown.
- env_
docs_ report schema - Renders versioned machine-readable environment variable documentation.
- env_
docs_ report_ json schema - Renders versioned environment variable documentation as JSON.
- env_
docs_ report_ json_ pretty schema - Renders versioned environment variable documentation as pretty JSON.
- json_
schema_ for schema - Exports the JSON Schema for a configuration type.
- json_
schema_ pretty schema - Exports the JSON Schema for a configuration type as pretty JSON.
Derive Macros§
- Json
Schema schema - Re-export of
schemars::JsonSchemaused bytierschema helpers. Derive macro forJsonSchematrait. - Tier
Config derive - Derives
tier::TierMetadatafor nested configuration structs. - Tier
Patch derive - Derives
tier::TierPatchfor typed sparse override structs.