Config loader for V with support for:
- JSON config files
- environment variable overrides
.envfile loading- nested struct field mapping
- python-dotenv-style parsing for env files
cfgman is useful when you want a typed config struct in V and want values to come from both a config file and environment variables.
- Load JSON into a typed V struct
- Override struct fields from environment variables
- Load variables from a
.envfile before applying overrides - Support nested structs with prefix expansion
- Parse
.envvalues with support for:- comments
export KEY=value- quoted values
- inline comments for unquoted values
${VAR}expansion from earlier parsed variables or the process environment
v install --git https://github.com/Te4nick/cfgman.gitAdd import into .v files
import cfgmanGiven this config struct:
module main
import cfgman
struct AppConfig {
pub:
port int
addr string
}And a config.json file:
{
"port": 8080,
"addr": "127.0.0.1"
}You can load it like this:
module main
import cfgman
struct AppConfig {
pub:
port int
addr string
}
fn main() {
cfg := cfgman.load[AppConfig](cfg_file_path: 'config.json')!
println(cfg.port)
println(cfg.addr)
}cfgman.load[T]() applies sources in this order:
- Start with
T{} - If
cfg_load_fileis enabled, parse the JSON config file into the struct - If
env_load_fileis enabled, load variables from the.envfile into the process environment - If
env_overridesis enabled, apply matching environment variables onto the struct fields
Loads a typed config struct from JSON and environment variables.
cfg := cfgman.load[MyConfig](
cfg_file_path: 'config.json'
env_file_path: '.env'
env_load_file: true
env_prefix: 'MYAPP'
env_overrides: true
)!Parses a JSON file into a struct without applying any environment variables.
cfg := cfgman.parse_json_file[MyConfig]('config.json')!Parses raw .env text.
values := cfgman.env_values('PORT=8080\nHOST=127.0.0.1\n')!Parses a .env file and returns its key/value pairs without mutating the process environment.
values := cfgman.parse_env_file('.env')!
println(values['DATABASE_URL'])Loads a .env file into the process environment.
- When
overridesisfalse, existing process environment values are preserved. - When
overridesistrue, file values replace existing environment variables.
loaded := cfgman.load_env_file('.env', false)!
println(loaded)@[params]
pub struct CfgParams {
pub:
env_overrides bool = true
env_prefix string = 'CFGMAN'
env_load_file bool
env_file_path string = '.env'
cfg_load_file bool = true
cfg_file_path string = 'config.json'
}If true, environment variables override values loaded from JSON.
Prefix used to map environment variables to struct fields.
Default:
CFGMAN
A field named port becomes:
CFGMAN_PORT
If true, load_env_file() is called before environment overrides are applied.
Path to the .env file.
Default:
.env
If true, the JSON config file is loaded.
Path to the JSON config file.
Default:
config.json
Field names are mapped using:
<PREFIX>_<FIELD_NAME_UPPERCASE>
Example:
struct AppConfig {
pub:
port int
addr string
}With prefix MYAPP, the loader reads:
MYAPP_PORT
MYAPP_ADDR
Struct fields can declare cfgman attributes to control how cfgman treats environment overrides.
Supported forms:
@[cfgman: 'json']
@[cfgman: 'env']
@[cfgman: 'env:CUSTOM_NAME']
@[cfgman: 'both']
@[cfgman: '-']You attach these attributes directly to struct fields.
Loads the field from JSON only.
struct Config {
pub:
port int
api_key string @[cfgman: 'json']
}With this setup:
portcan still be overridden from envapi_keyis loaded only from JSON
Example:
{
"port": 8080,
"api_key": "from_json"
}CFGMAN_PORT=9000
CFGMAN_API_KEY=from_env
Result:
port = 9000
api_key = from_json
Loads the field from environment only.
JSON values for that field are ignored, and the field remains at its zero value if no matching env variable exists.
struct Config {
pub:
port int
token string @[cfgman: 'env']
}Example JSON:
{
"port": 8080,
"token": "ignored-json"
}If CFGMAN_TOKEN is not set:
token = ''
If CFGMAN_TOKEN=from_env:
token = from_env
Overrides the environment variable name for a field.
struct Config {
pub:
port int @[cfgman: 'env:CUSTOM_PORT']
addr string
}This field now reads CUSTOM_PORT instead of CFGMAN_PORT or <PREFIX>_PORT.
Example:
CUSTOM_PORT=7000
Marks the field as using the default behavior explicitly.
struct Config {
pub:
port int @[cfgman: 'both']
}This is equivalent to not setting the attribute.
Disables both JSON and env handling for the field.
The field is left unchanged at its default value unless you set it manually after loading.
struct Config {
pub:
computed string @[cfgman: '-']
}The cfgman attribute value is comma-separated, so you can combine source selection and a custom env name.
struct Config {
pub:
port int @[cfgman: 'both,env:APP_HTTP_PORT']
secret string @[cfgman: 'json']
computed string @[cfgman: '-']
}module main
import cfgman
struct Config {
pub:
port int @[cfgman: 'env:CUSTOM_PORT']
token string @[cfgman: 'json']
secret string @[cfgman: 'env']
computed string @[cfgman: '-']
host string
}
fn main() {
cfg := cfgman.load[Config](
cfg_file_path: 'config.json'
env_prefix: 'APP'
) or {
panic(err)
}
println(cfg.port)
println(cfg.token)
println(cfg.host)
}If:
{
"port": 8080,
"token": "json-token",
"secret": "ignored-json-secret",
"computed": "ignored-json-value",
"host": "127.0.0.1"
}And the environment contains:
CUSTOM_PORT=5050
APP_TOKEN=env-token
APP_SECRET=env-secret
APP_HOST=0.0.0.0
Then the result is:
port = 5050
token = json-token
secret = env-secret
computed = ''
host = 0.0.0.0
Nested structs extend the prefix recursively.
struct DatabaseConfig {
pub:
host string
port int
}
struct AppConfig {
pub:
debug bool
database DatabaseConfig
}With env_prefix: 'MYAPP', the supported variables are:
MYAPP_DEBUG
MYAPP_DATABASE_HOST
MYAPP_DATABASE_PORT
Example:
module main
import cfgman
struct DatabaseConfig {
pub:
host string
port int
}
struct AppConfig {
pub:
debug bool
database DatabaseConfig
}
fn main() {
cfg := cfgman.load[AppConfig](
cfg_file_path: 'config.json'
env_prefix: 'MYAPP'
) or {
panic(err)
}
println(cfg.debug)
println(cfg.database.host)
println(cfg.database.port)
}Environment variable values are assigned using JSON decoding for non-string fields.
That means these work naturally:
MYAPP_PORT=8080
MYAPP_DEBUG=true
MYAPP_RATIO=1.25
For string fields, the raw environment value is used directly.
Example:
struct AppConfig {
pub:
port int
debug bool
name string
}MYAPP_PORT=3000
MYAPP_DEBUG=true
MYAPP_NAME=demo
{
"port": 8080,
"addr": "127.0.0.1",
"database": {
"host": "localhost",
"port": 5432
}
}export MYAPP_PORT=9000
MYAPP_DATABASE_HOST=db.internalmodule main
import cfgman
struct DatabaseConfig {
pub:
host string
port int
}
struct AppConfig {
pub:
port int
addr string
database DatabaseConfig
}
fn main() {
cfg := cfgman.load[AppConfig](
cfg_file_path: 'config.json'
env_load_file: true
env_file_path: '.env'
env_prefix: 'MYAPP'
) or {
panic(err)
}
println(cfg)
}portbecomes9000from.envaddrstays127.0.0.1from JSONdatabase.hostbecomesdb.internalfrom.envdatabase.portstays5432from JSON
The parser supports a practical subset of python-dotenv-style syntax.
PORT=8080
HOST=127.0.0.1
DEBUG=true# full-line comment
PORT=8080 # inline commentexport APP_NAME=myappPASSWORD="pa#ss word"
MESSAGE="hello\nworld"TOKEN='raw value'EMPTY=APP_NAME=cfgman
URL=https://example.com/${APP_NAME}Expansion can resolve from:
- variables parsed earlier in the same
.envcontent - variables already present in the process environment
Example:
module main
import cfgman
import os
fn main() {
os.setenv('DOMAIN', 'example.com', true)
values := cfgman.env_values('APP=demo\nURL=https://\${DOMAIN}/\${APP}\n') or {
panic(err)
}
println(values['URL'])
}Output:
https://example.com/demo
Use parse_env_file() when you want to inspect values without mutating the environment:
module main
import cfgman
fn main() {
values := cfgman.parse_env_file('.env') or {
panic(err)
}
for k, v in values {
println('${k}=${v}')
}
}Use load_env_file() when you want a .env file to populate os.getenv() values:
module main
import cfgman
import os
fn main() {
cfgman.load_env_file('.env', false) or {
panic(err)
}
println(os.getenv('DATABASE_URL'))
}The env parser returns an error for malformed lines such as:
- missing
= - invalid key names
- unterminated quoted values
These points reflect the current implementation.
- JSON loading depends on
x.json2.decode - Environment overrides are applied to struct fields only
- Default field names map from V field names to uppercase env names
.envexpansion supports${VAR}syntax, not$VAR- Recursive or circular expansion is not specially handled
- Single-quoted values are treated as literal text
- Double-quoted values support basic escape sequences only:
\n,\r,\t