Skip to content

Write config back and export a schema

The mutable feature adds two associated items to Config<C>: write, which puts the current value on disk, and schema, which emits a JSON Schema for the config struct. They exist to back config set, config schema and config validate style subcommands.

Enable the feature and the derives

rtb-config = { version = "0.6", features = ["mutable"] }

Both items need more from C than the rest of the crate does:

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

#[derive(Debug, Default, Deserialize, Serialize, JsonSchema)]
struct MyConfig {
    host: String,
    port: u16,
}

Without Serialize and JsonSchema the methods are not callable and the compiler says the method `write` exists for struct `Config<MyConfig>`, but its trait bounds were not satisfied, with a note naming Serialize and JsonSchema as the unsatisfied bounds — usually a missing derive.

Write the current configuration to a file

cfg.write(std::path::Path::new("/etc/mytool/config.yaml"))?;

Missing parent directories are created. The format follows the extension:

Extension Format
.yml, .yaml YAML
.toml TOML
.json JSON
anything else, or none YAML

Write YAML or JSON if you intend to read the file back. ConfigBuilder::user_file parses every file as YAML, so a .toml file this method wrote cannot be loaded again by this crate.

Understand what gets written

write serialises the merged value — exactly what get() returns — not the file layer on its own. Three consequences to design around:

  • Environment overrides are baked in. If MYTOOL_PORT=9999 is set when you call write, port: 9999 is now in the file permanently, as if a human had typed it.
  • Embedded defaults become explicit. Every key gets written out, so a later release that changes a default no longer affects this user.
  • The existing file is replaced. Comments, key order and any hand-formatting in the user's file are gone.

For a config set command that edits one key, this is usually the wrong tool: it turns a one-key edit into a full rewrite of everything the process happened to be running with. Editing the user's file directly keeps the rest of it intact.

The write is also not atomic — a crash part-way leaves a truncated file. If that matters, serialise to a temporary file in the same directory and rename it into place.

Emit a JSON Schema for the config struct

let schema = Config::<MyConfig>::schema();
println!("{}", serde_json::to_string_pretty(&schema)?);

schema() is an associated function, so it needs no loaded config and can run before anything is read. The result is a serde_json::Value with the usual top-level keys — $schema, title, type and properties — generated fresh on each call. There is no caching; the call is cheap enough for CLI use.

This is what to wire to a config schema subcommand, and what to hand to an editor for YAML completion against your tool's config file.

Validate a value before writing it

Nothing in the crate does this for you. schema() produces the schema; validating a candidate value against it is the caller's job, using a JSON Schema validator of your choice.

ConfigError::Schema exists for exactly that step, so a validation failure in your own code has a home in the same error type — but no code path inside rtb-config ever returns it.

Next