Why developers put JSON in environment variables
When configuring containerized microservices in Kubernetes, Docker, AWS ECS, or Vercel, passing complex structured configuration (like an array of webhook endpoints or a dynamic role-permission map) as dozens of flat strings (CONFIG_WEBHOOK_0_URL, CONFIG_WEBHOOK_0_SECRET) is painful.
To keep deployments clean, developers frequently pass a single serialized JSON string:
# .env / Kubernetes ConfigMap
APP_PAYMENT_CONFIG='{"gateways":["stripe","adyen"],"defaultCurrency":"USD","retryLimit":3}'However, parsing JSON from environment variables is notoriously prone to silent boot crashes caused by unescaped newlines, shell quote stripping, trailing commas, or missing environment keys.
Here is how to safely parse and validate JSON environment variables across modern runtimes.
1. The Safe Parse Pattern with Fallbacks
Never write a raw JSON.parse(process.env.MY_CONFIG!) directly in module scope. A single syntax error will crash your entire process before error monitoring initializes.
import { z } from "zod";
const PaymentConfigSchema = z.object({
gateways: z.array(z.string()).min(1),
defaultCurrency: z.string().length(3),
retryLimit: z.number().int().min(1).default(3),
});
export type PaymentConfig = z.infer<typeof PaymentConfigSchema>;
export function loadPaymentConfig(): PaymentConfig {
const raw = process.env.APP_PAYMENT_CONFIG;
if (!raw || raw.trim() === "") {
throw new Error("Missing required environment variable: APP_PAYMENT_CONFIG");
}
try {
// 1. Parse JSON safely
const parsed = JSON.parse(raw);
// 2. Validate shape with Zod
return PaymentConfigSchema.parse(parsed);
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error(`Malformed JSON in APP_PAYMENT_CONFIG: ${error.message}`);
}
if (error instanceof z.ZodError) {
throw new Error(`Schema validation failed for APP_PAYMENT_CONFIG: ${JSON.stringify(error.issues)}`);
}
throw error;
}
}2. Handling Multiline and Base64-Encoded JSON
If your JSON payload contains RSA private keys, certificates, or multiline strings, shell variables frequently strip quotes or corrupt whitespace.
The production-proven pattern is to Base64-encode the JSON payload:
# Encode on your local machine:
# cat config.json | base64
APP_ENCODED_CONFIG="eyJnYXRld2F5cyI6WyJzdHJpcGUiXSwicmV0cnlMaW1pdCI6M30="In your runtime:
export function loadBase64Json<T>(envVarName: string): T {
const base64Str = process.env[envVarName];
if (!base64Str) throw new Error(`${envVarName} is undefined`);
// Decode Base64 string to utf-8 text
const jsonText = Buffer.from(base64Str, "base64").toString("utf-8");
return JSON.parse(jsonText) as T;
}3. Native Environment Handling in Bun and Deno
In Bun:
// Bun loads .env automatically with zero dependencies
const rawConfig = Bun.env.APP_PAYMENT_CONFIG;In Deno:
// Deno requires explicit permission flags (--allow-env)
const rawConfig = Deno.env.get("APP_PAYMENT_CONFIG");Helpful Tools for Configuration Management
- Base64 Encoder / Decoder: Encode JSON config payloads for safe shell transmission.
- JSON Formatter & Validator: Validate your
.envJSON payloads before committing deployment manifests. - JSON Schema Validator: Verify config structures against production schemas.