environment-variablesconfignodebundenosecuritydevtools

Safely Parsing JSON from Environment Variables in Node, Bun, and Deno

·7 min read·Developer Tools

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:

bash
# .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.

typescript
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:

bash
# Encode on your local machine:
# cat config.json | base64
APP_ENCODED_CONFIG="eyJnYXRld2F5cyI6WyJzdHJpcGUiXSwicmV0cnlMaW1pdCI6M30="

In your runtime:

typescript
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:

typescript
// Bun loads .env automatically with zero dependencies
const rawConfig = Bun.env.APP_PAYMENT_CONFIG;

In Deno:

typescript
// Deno requires explicit permission flags (--allow-env)
const rawConfig = Deno.env.get("APP_PAYMENT_CONFIG");

Helpful Tools for Configuration Management

Frequently asked questions

Sensitive values (passwords, private API keys) should ideally be injected via dedicated secrets managers (AWS Secrets Manager, HashiCorp Vault) rather than static unencrypted environment files.

JSON specification (RFC 8259) strictly requires double quotes ("key": "value"). If your shell passes {'key': 'value'}, JSON.parse() will throw a syntax error.

Use Zod's .default() modifier or provide a static fallback object when process.env[KEY] is undefined.

Try Base64 Encoder & JSON Validator

Encode structured config payloads to Base64 and validate .env JSON syntax.