Skip to content

Wire a typed configuration

Without this, config show prints a placeholder, config schema errors, and config set writes whatever you give it unvalidated. Wiring a typed config turns all five subcommands on.

Define the config type

Four traits are required: Serialize, DeserializeOwned, JsonSchema, and Send + Sync + 'static.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MyConfig {
    /// Where the API lives.
    pub endpoint: String,
    /// Request timeout in seconds.
    #[serde(default = "default_timeout")]
    pub timeout_secs: u64,
}

fn default_timeout() -> u64 {
    30
}

The JsonSchema bound is what makes config schema and the validation paths work. It is not optional — a tool whose config shape cannot derive JsonSchema has to stay on the untyped path.

Hand it to the builder

let config: rtb_config::Config<MyConfig> = /* built by rtb-config */;

Application::builder()
    .metadata(metadata)
    .version(rtb_app::version_info!())
    .config(config)
    .build()?
    .run()
    .await

Loading and layering the configuration is rtb-config's job. .config(...) is the step that makes the result visible to the config command and to your own command handlers.

Read it from a command

async fn run(&self, app: App) -> miette::Result<()> {
    let config = app
        .typed_config::<MyConfig>()
        .ok_or_else(|| miette::miette!("typed config not wired"))?;
    // …
}

typed_config::<C>() returns None when nothing was wired, or when the type requested is not the type that was.

What changes once it is wired

Command Untyped Typed
config show Placeholder comment lines The merged value, as YAML
config get Reads the user file Reads the merged value, defaults and env overlays included
config schema Fails Prints the JSON Schema
config validate Parse check only Full schema validation
config set Writes unvalidated Validates the candidate first; rejects on failure

The get change is the one that catches people out: untyped get shows what is in the file, typed get shows what the tool actually sees. A value that comes from an embedded default appears only in the typed reading.

Verify it

$ mytool config schema | head -5
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "MyConfig",
  ...

$ mytool config set /timeout_secs '"thirty"'
config set rejected: candidate value at `/timeout_secs` fails the wired schema: ...

If config schema still errors, .config(...) did not run — check it is on the builder chain before .build().

  • config reference — every subcommand, the file paths, the format rules.
  • rtb-config — building the Config<C> this step consumes.