---
title: API reference
description: Every public type and method in rtb-config — signatures, trait bounds, return values, and the exact conditions under which each one fails.
---
# API reference
Everything `rtb-config` exposes, with the behaviour each item actually has. Verified
against the crate source at v0.6.3.
## Does rtb-config have its own configuration keys?
No. `rtb-config` has no keys, no flags and no environment variables of its own. The
keys are the field names of *your* `serde::Deserialize` struct, and the environment
variables are whatever prefix you register with
[`ConfigBuilder::env_prefixed`](#configbuilderenv_prefixed-read-prefixed-environment-variables).
The knobs the crate itself owns are its two Cargo features — see
[Cargo features](cargo-features.md).
## What the crate exports
| `Config<C = ()>` | struct | — | Holds the parsed value and the sources it came from |
| `ConfigBuilder<C>` | struct | — | Registers sources, then parses them |
| `ConfigError` | enum | — | Every failure the crate can return |
| `WatchHandle` | struct | `hot-reload` | Owns a running file watcher; drop it to stop |
The modules `rtb_config::config`, `rtb_config::error` and (under `hot-reload`)
`rtb_config::watch` are public, but everything worth naming is re-exported at the
crate root:
```rust
use rtb_config::{Config, ConfigBuilder, ConfigError};
#[cfg(feature = "hot-reload")]
use rtb_config::WatchHandle;
```
## Which trait bounds does my config struct need?
`Config<C>` requires `C: DeserializeOwned + Send + Sync + 'static` everywhere. Two
methods ask for more, and they only exist when the bound is met:
| `Config::write` | `serde::Serialize`, `schemars::JsonSchema` | `mutable` |
| `Config::schema` | `serde::Serialize`, `schemars::JsonSchema` | `mutable` |
| `Config::default` | `Default` | — |
If `Config::<MyConfig>::write` does not exist for your type, the missing piece is
almost always `#[derive(Serialize, JsonSchema)]` rather than the feature flag.
`C` defaults to `()`, so `Config` with no angle brackets means `Config<()>`. That
exists so downstream code holding an `Arc<Config>` does not have to carry a type
parameter it never uses.
## `Config::builder` — start registering sources
```rust
pub fn builder() -> ConfigBuilder<C>
```
Equivalent to `ConfigBuilder::<C>::new()`. Nothing is read from disk or the
environment until [`build`](#configbuilderbuild-parse-every-source-and-construct-the-config)
is called.
## `Config::with_value` — wrap a value with no sources behind it
```rust
pub fn with_value(value: C) -> Self
```
Stores `value` directly. The resulting `Config` has an **empty source list**, which
has one consequence worth knowing before you use it in a test:
calling `reload()` on it re-parses nothing, so the stored value is replaced by
whatever an empty source set deserialises into. For a struct whose fields all carry
`#[serde(default)]` that is `C::default()` — the value you passed in is gone. For a
struct with a required field it is `ConfigError::Parse`.
Use `builder().embedded_default(...)` instead when a test needs a value that
survives a reload.
## `Config::get` — take a snapshot of the current value
```rust
pub fn get(&self) -> Arc<C>
```
Returns a snapshot. Cheap: an atomic load and an `Arc` clone, no parsing, no locks.
A snapshot is immutable and complete. Hold one across a `reload()` and you keep
seeing the pre-reload value for as long as you hold it; the next `get()` returns the
post-reload value. There is no state in which half the fields are old and half are
new.
## `Config::reload` — re-read every source
```rust
pub fn reload(&self) -> Result<(), ConfigError>
```
Re-reads **every registered source** — embedded strings, files, and environment
variables — merges them again, and swaps the result in atomically. Environment
variables are read at reload time, not cached from `build()`, so a variable set
after startup takes effect on the next reload.
On success, every [`subscribe`](#configsubscribe-be-woken-when-the-value-changes)
receiver is woken.
On failure the stored value is **left untouched** and no subscriber is woken. A
config file that has been saved mid-edit and does not parse cannot take a running
process down to defaults.
## `Config::subscribe` — be woken when the value changes
```rust
pub fn subscribe(&self) -> tokio::sync::watch::Receiver<Arc<C>>
```
The receiver holds the current value immediately — `rx.borrow()` is valid before any
reload has happened — and `rx.changed().await` resolves after each *successful*
reload.
Subscribing late is safe. A receiver created after several reloads sees the newest
value, not the value the process started with, and dropping every receiver does not
break subsequent reloads.
## `Config::watch_files` — reload automatically when a file changes
```rust
#[cfg(feature = "hot-reload")]
pub fn watch_files(&self) -> Result<WatchHandle, ConfigError>
```
Starts one background thread (named `rtb-config-watcher`) watching every path
registered with `user_file`, non-recursively. File-system events are debounced for
**250 ms** — a fixed constant, not configurable — and each batch calls `reload()`.
A failing reload is swallowed so a bad save does not kill the watcher.
Returns `ConfigError::Watch` when:
- no `user_file` path was registered, or
- a registered path **does not exist yet** — the underlying watcher refuses to watch
a path that is not there, so create the file before calling this, or
- the OS refuses another watch (handle limits, unsupported filesystem).
The handle is `#[must_use]`: dropping it immediately stops the watcher. Keep it alive
for as long as you want reloads.
Read [the limitations](../explanation/limitations.md#which-file-changes-does-the-watcher-actually-see)
before relying on this in production — a save that *replaces* the file rather than
writing into it stops the watcher permanently.
## `Config::schema` — emit a JSON Schema for the config struct
```rust
#[cfg(feature = "mutable")]
pub fn schema() -> serde_json::Value
```
An associated function — there is no `&self`, so it can be called before any config
is loaded. Generates the schema from `C`'s `JsonSchema` derive at call time; there
is no caching, and it is cheap enough for CLI-startup use.
The returned object carries the usual top-level keys — `$schema`, `title`, `type`
and `properties`. If serialisation somehow fails, the function returns
`serde_json::Value::Null` rather than panicking.
## `Config::write` — write the current value to a file
```rust
#[cfg(feature = "mutable")]
pub fn write(&self, path: &Path) -> Result<(), ConfigError>
```
Serialises the **merged, current** value — the same thing `get()` returns — and
writes it to `path`. Missing parent directories are created.
Format is chosen by extension:
| `.yml`, `.yaml` | YAML |
| `.toml` | TOML |
| `.json` | JSON |
| anything else, or none | YAML |
Two things to know before you wire this to a `config set` command:
- **What is written is the merged result**, not just the file layer. Any environment
override active at the time is baked into the file as a literal value, and so are
the embedded defaults. Comments and key ordering in an existing file are not
preserved — the file is replaced.
- **Only the YAML output can be read back** by `ConfigBuilder::user_file`, which
parses every file as YAML. JSON survives the round trip because YAML is a superset
of it; TOML does not. See
[limitations](../explanation/limitations.md#can-i-use-a-toml-or-json-config-file).
The write is not atomic. If a torn file would matter, write to a temporary path and
rename it yourself.
Errors are `ConfigError::Write` for both serialisation and filesystem failures,
including failing to create the parent directory.
## `Config` trait implementations
| `Clone` | Cheap. Clones share the same stored value, sources and subscriber channel — a reload through one handle is visible through all of them. |
| `Default` | Requires `C: Default`. Stores `C::default()` with **no sources**, so it behaves like `with_value` for reload purposes. |
| `Debug` | Prints the source inventory only — file paths, env prefixes and the number of embedded layers. The stored value is deliberately never rendered, so a `Debug` log of a `Config` cannot leak a secret held in a config field. |
Example of the `Debug` output:
```text
Config { files: ["/etc/mytool/config.yaml"], env_prefixes: ["MYTOOL_"], embedded_layers: 1, .. }
```
## `ConfigBuilder::embedded_default` — bake defaults into the binary
```rust
pub fn embedded_default(mut self, yaml: &'static str) -> Self
```
Takes a `&'static str` of YAML, which in practice means a literal or an
`include_str!`. Call it more than once and the layers merge in call order, the later
call winning on any key both define.
## `ConfigBuilder::user_file` — read a YAML file from disk
```rust
pub fn user_file(mut self, path: impl Into<PathBuf>) -> Self
```
Registers a file path. Paths are read in registration order and later files win on
shared keys.
| File does not exist | Not an error — contributes no keys |
| File exists and parses | Merged over the layers before it |
| File exists, is not valid YAML | `ConfigError::Parse` |
| Path exists but is a directory | `ConfigError::Io`, carrying the path |
| File exists but is unreadable (permissions) | `ConfigError::Parse` — **not** `Io` |
The file is always parsed as YAML regardless of its extension.
## `ConfigBuilder::env_prefixed` — read prefixed environment variables
```rust
pub fn env_prefixed(mut self, prefix: impl Into<String>) -> Self
```
Registers an environment-variable layer. Matching is **case-insensitive** on the
variable name, the prefix is stripped, the remainder is lower-cased, and each
remaining underscore becomes a level of nesting:
| `MYTOOL_PORT=8080` | `MYTOOL_` | `port` |
| `MYTOOL_HTTP_PORT=8080` | `MYTOOL_` | `http.port` |
| `mytool_port=8080` | `MYTOOL_` | `port` |
Values are parsed, not taken as raw strings: `true` becomes a boolean, `8080` a
number, and `[alpha,beta]` a two-element list. A value that cannot be parsed into the
field's type fails with `ConfigError::Parse`, naming both the key and the prefix.
Two things bite here, both silent:
- **Include the trailing underscore in the prefix.** `env_prefixed("MYTOOL")` with
`MYTOOL_PORT` set leaves a stray leading underscore on the key and the variable is
ignored — no error, no warning.
- **A field name containing an underscore cannot be set from the environment at
all**, because that underscore is read as nesting. See
[limitations](../explanation/limitations.md#why-does-my-underscored-field-ignore-its-environment-variable).
Register several prefixes if you want to accept more than one; later registrations
win.
## `ConfigBuilder::build` — parse every source and construct the Config
```rust
pub fn build(self) -> Result<Config<C>, ConfigError>
```
Reads and merges every registered source, deserialises the result into `C`, and
returns the `Config`. This is the point at which files are opened and environment
variables are read — the builder methods only record intent.
A builder with no sources at all is valid. It deserialises an empty document, which
succeeds for a struct whose fields all have defaults and fails with
`ConfigError::Parse` for one that has a required field.
## In what order are sources merged?
**Embedded defaults, then files, then environment variables** — always, regardless
of the order you called the builder methods in. Registering `.env_prefixed()` before
`.embedded_default()` does not make the embedded layer win; the environment still
overrides it.
Within a single kind, registration order applies and the last one wins.
Merging is per key and goes into nested maps: a file that sets only `http.port`
leaves `http.host` from the layer beneath it intact. Sequences are **not** merged —
a list in a later layer replaces the whole list, it does not append to it.
## What happens to a key my struct does not have?
It is ignored, silently and successfully. `port: 8080` next to a misspelled
`tiemout: 30` deserialises fine and the typo does nothing. `rtb-config` has no strict
mode of its own that turns unknown keys into errors.
`#[serde(deny_unknown_fields)]` on your struct does turn them into a
`ConfigError::Parse`, and it works through every layer — but it is blunter than it
looks once an environment layer is registered. Every variable sharing the prefix is
a candidate key, so an unrelated `MYTOOL_SOMETHING_ELSE` in the environment fails the
build with `unknown field: found \`something\``. Reserve the prefix for this tool
before turning strict mode on.
## Next
- [Error reference](errors.md) — every variant, its diagnostic code, and what causes it
- [Cargo features](cargo-features.md) — what each feature adds and costs
- [Limitations](../explanation/limitations.md) — what the crate deliberately does not do