Environment variable schema validation and management for Rust
env-schema provides a declarative approach to managing environment variables in Rust applications. Define your configuration schema once, and get validation, documentation generation, and type-safe loading for free.
Three core benefits:
-
📋 Contract: Your schema is the single source of truth. Environment variables are validated against the schema at startup, catching configuration errors early.
-
📚 Documentation: Automatically generate
.env.examplefiles and Markdown documentation from your schema. Keep your docs in sync with your code. -
🛡️ Strict Mode: Optionally enforce that only known environment variables are present, preventing typos and accidental configuration drift.
[dependencies]
env-schema = "0.1.0"
env-schema-derive = "0.1.0"use env_schema::prelude::*;
use env_schema::resolve::LoadMode;
use env_schema::secret::Secret;
use env_schema_derive::EnvSchema;
#[derive(EnvSchema, Debug)]
struct AppConfig {
/// Database connection URL
#[env(required, secret, example = "postgres://user:password@localhost:5432/mydb")]
database_url: Secret<String>,
/// Server port number
#[env(default = "3000", range(min = 1.0, max = 65535.0))]
port: u16,
/// Application environment
#[env(default = "development", values = ["development", "staging", "production"])]
environment: String,
}fn main() -> Result<()> {
let config = AppConfig::load(LoadMode::Lenient)?;
println!("Database: {}", config.database_url.expose());
println!("Port: {}", config.port);
println!("Environment: {}", config.environment);
Ok(())
}- Required fields: Ensure critical configuration is present
- Default values: Provide sensible defaults for optional settings
- Type parsing: Automatic parsing to
String,u16,u32,u64,i32,i64,bool,Option<T>, and custom types viaFromStr - Range constraints: Validate numeric values within bounds
- Value constraints: Restrict to specific allowed values
- Regex patterns: Validate string formats
- Strict mode: Reject unknown environment variables
- Secret wrapper:
Secret<T>redacts sensitive values inDebugandDisplayoutput - Secret fields: Mark fields as secrets to prevent accidental logging
.env.examplefiles: Auto-generate example environment files- Markdown docs: Generate comprehensive configuration documentation
- Comments and examples: Include field descriptions and example values
- Derive macro: Define schemas using struct attributes
- Builder API: Programmatically construct schemas
- Clear error messages: Detailed validation errors with context
- Aliases: Support legacy environment variable names
- Deprecations: Warn when deprecated keys are used
See INTEGRATIONS.md for:
- 🔌 Integration examples with popular crates (
dotenv,serde,config-rs,tokio,clap) - 📖 Migration guides from other environment variable crates
- 📊 Comprehensive comparison table with alternatives
When validation fails, env-schema provides clear, actionable error messages:
Configuration validation failed with 3 error(s):
1. Missing required environment variable: `DATABASE_URL`
2. Value for `PORT` is out of range: `99999` (expected 1 to 65535)
3. Value for `ENVIRONMENT` does not match pattern `^(dev|staging|prod)$`: `invalid`
Errors are collected and reported together, so you can fix all issues in one pass.
Generate documentation from your schema:
use env_schema_derive::EnvSchema;
#[derive(EnvSchema)]
struct AppConfig {
#[env(required, secret, example = "postgres://localhost/db")]
database_url: Secret<String>,
#[env(default = "3000", doc = "Server port")]
port: u16,
}
fn main() {
let schema = AppConfig::schema();
// Generate .env.example
let env_example = schema.render_env_example();
std::fs::write(".env.example", env_example).unwrap();
// Generate Markdown documentation
let docs = schema.render_docs_md();
std::fs::write("CONFIG.md", docs).unwrap();
}# Database connection URL
# Required
DATABASE_URL=<secret>
# Server port
PORT=3000
# Configuration
## How to Use
Set environment variables before running the application...
## Environment Variables
| Key | Required | Default | Example | Description | Constraints |
|-----|----------|---------|---------|-------------|-------------|
| `DATABASE_URL` | **Yes** | — | `<secret>` | Database connection URL | — |
| `PORT` | No | `3000` | — | Server port | Range: 1 to 65535 |The Secret<T> wrapper type prevents accidental exposure of sensitive values:
use env_schema::secret::Secret;
let api_key = Secret("my-secret-key".to_string());
// Safe: redacts the value
println!("{:?}", api_key); // prints: <redacted>
println!("{}", api_key); // prints: <redacted>
// Explicit access when needed
let actual_key = api_key.expose();
⚠️ Important:Secret<T>only prevents accidental logging viaDebugandDisplay. The value is still stored in memory. For production secrets, use proper secret management systems and never commit secrets to version control.
Enable optional features in Cargo.toml:
[dependencies]
env-schema = { version = "0.1.0", features = ["json", "url", "humantime"] }-
yaml(optional): Enables loading schemas from YAML files. Allows tools likeenvctlto consume schemas without needing Rust derive macros. -
json(optional): Enables JSON error report rendering viareport::render_json(). Useful for programmatic consumption of error reports in structured format. -
url(optional): Enables parsing ofUrltypes from theurlcrate. Allows environment variables to be parsed directly intourl::Urltypes with validation. -
humantime(optional): Enables parsing ofDurationtypes using human-readable strings. Supports formats like"5s","10m","1h","2d", or combinations like"1h30m15s".
You can load a schema from a YAML file or string, which is useful for tools that need to consume schemas without Rust code.
Add the yaml feature to your Cargo.toml:
[dependencies]
env-schema = { version = "0.1.0", features = ["yaml"] }A YAML schema can be defined with a top-level fields: array (preferred) or as a direct array:
fields:
- key: "DB_URL"
required: true
secret: true
doc: "Postgres connection string"
example: "postgres://user:pass@host:5432/db"
default: null
aliases: ["DATABASE_URL"]
deprecated: false
range:
min: 1
max: 200
values: null
regex: null
- key: "PORT"
required: false
default: "3000"
doc: "Server port"
range:
min: 1
max: 65535use env_schema::schema::Schema;
// Load from a file
let schema = Schema::from_yaml_file("schema.yaml")?;
// Or load from a string
let yaml = r#"
fields:
- key: "DB_URL"
required: true
secret: true
"#;
let schema = Schema::from_yaml_str(yaml)?;
// Use the schema as normal
let config = schema.load_map(LoadMode::Strict)?;key(required): The environment variable name (must be non-empty)required(default:false): Whether the variable must be presentsecret(default:false): Whether the variable contains sensitive datadoc(optional): Human-readable documentationexample(optional): Example valuedefault(optional): Default value (nullmeans no default)aliases(optional): Array of alternative variable namesdeprecated(default:false): Whether the field is deprecateddeprecated_message(optional): Deprecation message (auto-generated ifdeprecated: trueand message is empty)range(optional): Numeric range constraint withminandmaxvalues(optional): Array of allowed values (enum constraint)regex(optional): Regular expression pattern constraint
The YAML loader performs comprehensive validation:
- Duplicate keys are rejected
- Empty keys are rejected
- Aliases cannot equal the main key
- Range constraints require
min <= max - Values constraints must have at least one item
- Regex patterns are compile-checked
All validation errors are aggregated and returned together for easy debugging.
See examples/schema.yaml for a complete example.
See examples/web_service.rs for a complete example with:
- 🌐 Network configuration (listen address, base URL)
- 🗄️ Database connection pooling
- ⏱️ Timeout configuration
- 🔀 CORS settings
- 📊 Observability (Sentry, OpenTelemetry)
Licensed under the MIT license (LICENSE-MIT)