cfgman
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
Features
- 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
Installation
v install --git https://github.com/Te4nick/cfgman.git
Add import into .v files
import cfgman
Quick Start
Given this config struct:
module main
import cfgman
struct AppConfig {
pub:
port int
addr string
}
And a
config.json
{
"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)
}
How Loading Works
cfgman.load[T]()
- 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
Primary API
load[T](params CfgParams) !T
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
)!
parse_json_file[T](path string) !T
Parses a JSON file into a struct without applying any environment variables.
cfg := cfgman.parse_json_file[MyConfig]('config.json')!
env_values(data string) !map[string]string
Parses raw
.env
values := cfgman.env_values('PORT=8080\nHOST=127.0.0.1\n')!
parse_env_file(path string) !map[string]string
Parses a
.env
values := cfgman.parse_env_file('.env')!
println(values['DATABASE_URL'])
load_env_file(path string, overrides bool) !map[string]string
Loads a
.env
- When
overridesis false, existing process environment values are preserved. - When
overridesis true, file values replace existing environment variables.
loaded := cfgman.load_env_file('.env', false)!
println(loaded)
CfgParams
@[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'
}
Fields
env_overrides
If
true
env_prefix
Prefix used to map environment variables to struct fields.
Default:
CFGMAN
A field named
port
CFGMAN_PORT
env_load_file
If
true
load_env_file()
env_file_path
Path to the
.env
Default:
.env
cfg_load_file
If
true
cfg_file_path
Path to the JSON config file.
Default:
config.json
Environment Variable Mapping
Field names are mapped using:
<PREFIX>_<FIELD_NAME_UPPERCASE>
Example:
struct AppConfig {
pub:
port int
addr string
}
With prefix
MYAPP
MYAPP_PORT
MYAPP_ADDR
Field Attributes
Struct fields can declare
cfgman
cfgman
Supported forms:
@[cfgman: 'json']
@[cfgman: 'env']
@[cfgman: 'env:CUSTOM_NAME']
@[cfgman: 'both']
@[cfgman: '-']
You attach these attributes directly to struct fields.
@[cfgman: 'json']
Loads the field from JSON only.
struct Config {
pub:
port int
api_key string @[cfgman: 'json']
}
With this setup:
-
portcan still be overridden from env -
api_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
@[cfgman: 'env']
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
token = ''
If
CFGMAN_TOKEN=from_env
token = from_env
@[cfgman: 'env:CUSTOM_NAME']
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
CFGMAN_PORT
<PREFIX>_PORT
Example:
CUSTOM_PORT=7000
@[cfgman: 'both']
Marks the field as using the default behavior explicitly.
struct Config {
pub:
port int @[cfgman: 'both']
}
This is equivalent to not setting the attribute.
@[cfgman: '-']
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: '-']
}
Combined usage
The
cfgman
struct Config {
pub:
port int @[cfgman: 'both,env:APP_HTTP_PORT']
secret string @[cfgman: 'json']
computed string @[cfgman: '-']
}
Attribute Example
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
Nested structs extend the prefix recursively.
struct DatabaseConfig {
pub:
host string
port int
}
struct AppConfig {
pub:
debug bool
database DatabaseConfig
}
With
env_prefix: 'MYAPP'
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)
}
Type Conversion Rules
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
Full Example
config.json
{
"port": 8080,
"addr": "127.0.0.1",
"database": {
"host": "localhost",
"port": 5432
}
}
.env
export MYAPP_PORT=9000
MYAPP_DATABASE_HOST=db.internal
V code
module 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)
}
Result
-
portbecomes 9000from .env -
addrstays 127.0.0.1from JSON -
database.hostbecomes db.internalfrom .env -
database.portstays 5432from JSON
Working With
.env
Files
The parser supports a practical subset of python-dotenv-style syntax.
Basic assignments
PORT=8080
HOST=127.0.0.1
DEBUG=true
Comments
# full-line comment
PORT=8080 # inline comment
export
export APP_NAME=myapp
Double-quoted values
PASSWORD="pa#ss word"
MESSAGE="hello\nworld"
Single-quoted values
TOKEN='raw value'
Empty values
EMPTY=
Variable expansion
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
Parsing
.env
Without Loading It
Use
parse_env_file()
module main
import cfgman
fn main() {
values := cfgman.parse_env_file('.env') or {
panic(err)
}
for k, v in values {
println('${k}=${v}')
}
}
Loading
.env
Into The Process Environment
Use
load_env_file()
.env
os.getenv()
module main
import cfgman
import os
fn main() {
cfgman.load_env_file('.env', false) or {
panic(err)
}
println(os.getenv('DATABASE_URL'))
}
Error Cases
The env parser returns an error for malformed lines such as:
- missing
= - invalid key names
- unterminated quoted values
Current Limitations
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