xarunoba/env-struct
🌱 — Parse environment variables into structs in Zig
env-struct
— environment variables to typed structs
A Zig library for parsing environment variables directly into typed structs, providing automatic type conversion and validation.
This library does not read environment variables from files; it only parses existing environment variables into a struct.
Managing configuration with environment variables is common, but environment variables are always strings and require manual parsing and validation. env-struct
eliminates boilerplate by mapping environment variables directly to typed Zig structs, providing automatic type conversion and validation at load time. This approach improves safety, reduces errors, and makes configuration handling more robust and maintainable.
This is my first ever Zig project so feel free to contribute and send PRs!
zig fetch
(Recommended)Add this library to your project using zig fetch
:
zig fetch --save "git+https://github.com/xarunoba/env-struct#main"
Then in your build.zig
:
const env_struct = b.dependency("env_struct", .{});
exe.root_module.addImport("env_struct", env_struct.module("env_struct"));
Alternatively, you can directly copy the env_struct.zig
file from the src/
directory into your project and import it locally to prevent any external dependencies:
const env_struct = @import("env_struct.zig");
const std = @import("std");
const env_struct = @import("env_struct");
const Config = struct {
APP_NAME: []const u8, // Maps to "APP_NAME" env var
PORT: u32, // Maps to "PORT" env var
DEBUG: bool = false, // Maps to "DEBUG" env var, defaults to false
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}).init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const config = try env_struct.load(Config, allocator);
std.debug.print("App: {s}\n", .{config.APP_NAME});
std.debug.print("Port: {}\n", .{config.PORT});
}
Set environment variables:
export APP_NAME="My App"
export PORT="8080"
const Config = struct {
name: []const u8,
port: u32,
debug: bool = false,
timeout: ?f32 = null,
const env = .{
.name = "APP_NAME",
.port = "PORT",
.debug = "DEBUG",
.timeout = "TIMEOUT",
};
};
const config = try env_struct.load(Config, allocator);
Set environment variables:
export APP_NAME="My App"
export PORT="8080"
const Config = struct {
app_name: []const u8, // Maps to "app_name" env var
custom_port: u32, // Maps to "PORT" env var (custom mapping)
debug: bool = false, // Maps to "debug" env var, uses default
internal_field: []const u8 = "computed", // Skipped from env lookup
optional_feature: ?u32, // Maps to "optional_feature", can be null
const env = .{
.custom_port = "PORT", // Custom environment variable name
.internal_field = "-", // Skip environment variable lookup
};
};
const DatabaseConfig = struct {
host: []const u8,
port: u32 = 5432,
const env = .{
.host = "DB_HOST",
.port = "DB_PORT",
};
};
const ServerConfig = struct {
host: []const u8 = "localhost",
port: u32,
database: DatabaseConfig,
const env = .{
.host = "SERVER_HOST",
.port = "SERVER_PORT",
};
};
// Load from system environment
const config = try env_struct.load(ServerConfig, allocator);
// Or load from custom environment map (useful for testing)
var custom_env = std.process.EnvMap.init(allocator);
defer custom_env.deinit();
try custom_env.put("SERVER_PORT", "3000");
const test_config = try env_struct.loadMap(ServerConfig, custom_env, allocator);
Fields are mapped to environment variables with these behaviors:
env
declaration to map fields to different environment variable names "-"
to skip environment variable lookup (must have default values or be optional)env
declaration is only needed for custom mappings or skipping fieldsconst Config = struct {
app_name: []const u8, // Maps to "app_name" env var
custom_port: u32, // Maps to "PORT" env var
skipped_field: []const u8 = "default", // No env var lookup
const env = .{
.custom_port = "PORT", // Custom mapping
.skipped_field = "-", // Skip mapping
// app_name uses default mapping
};
};
Type | Examples | Notes |
---|---|---|
[]const u8 |
"hello" |
String values |
i8 , i16 , i32 , i64 , i128 , isize |
"42" , "-123" |
Signed integers |
u8 , u16 , u32 , u64 , u128 , usize |
"42" , "255" |
Unsigned integers |
f32 , f64 |
"3.14" |
Floating point |
bool |
"true" , "1" , "yes" |
Case-insensitive |
?T |
Any valid T or missing |
Optional types |
struct |
N/A | Nested structs |
load(comptime T: type, allocator: std.mem.Allocator) !T
Load configuration from system environment variables.
loadMap(comptime T: type, env_map: std.process.EnvMap, allocator: std.mem.Allocator) !T
Load configuration from a custom environment map.
zig build
zig test src/env_struct.zig