Free & open source — no account required
Free & open source — no account required
This technical guide provides an in-depth analysis of the dotenv to json engine, best practices for implementation, and data security standards.
Converting a .env file to JSON is the reverse of the usual pattern — JSON is typically used for machine-to-machine config, while .env is for local developer overrides. The conversion matters for several real use cases: exporting environment variables to a JSON format accepted by AWS Secrets Manager, Azure Key Vault, or Docker Compose; normalizing multiple .env files (staging, production) into a structured JSON config object for validation; and building a config layer that merges .env variables with JSON defaults. This guide covers .env parsing with dotenv-flow, prefix-based grouping into nested JSON, type coercion rules, and safe patterns for CI/CD secret injection.
// .env file (common local development format)
# Server configuration
PORT=3000
HOST=0.0.0.0
NODE_ENV=development
LOG_LEVEL=debug
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=appdb
DB_USER=postgres
DB_PASSWORD=local_dev_password
DB_POOL_MIN=2
DB_POOL_MAX=10
DB_SSL=false
# External APIs
STRIPE_SECRET_KEY=sk_test_4eC39HqLyjWDarjtT1zdp7dc
STRIPE_WEBHOOK_SECRET=whsec_abc123
SENDGRID_API_KEY=SG.abc123.xyz
REDIS_URL=redis://localhost:6379
# Feature flags
FEATURE_NEW_CHECKOUT=true
FEATURE_ANALYTICS=false
MAX_FILE_SIZE_MB=50
// Parse and export to structured JSON
import dotenv from 'dotenv';
const env = dotenv.parse(readFileSync('.env', 'utf-8'));
// Coerce types: numbers and booleans
function coerceValue(v: string): string | number | boolean {
if (v === 'true') return true;
if (v === 'false') return false;
const n = Number(v);
if (!isNaN(n) && v.trim() !== '') return n;
return v;
}
const coerced = Object.fromEntries(
Object.entries(env).map(([k, v]) => [k, coerceValue(v)])
);
console.log(JSON.stringify(coerced, null, 2));
// {
// "PORT": 3000,
// "HOST": "0.0.0.0",
// "NODE_ENV": "development",
// "DB_HOST": "localhost",
// "DB_PORT": 5432,
// ...
// "FEATURE_NEW_CHECKOUT": true,
// "DB_SSL": false
// }
// Group env vars by prefix (DB_ → database, STRIPE_ → stripe, etc.)
function groupByPrefix(env: Record<string, unknown>): Record<string, unknown> {
const prefixMap: Record<string, string> = {
'DB_': 'database',
'REDIS_': 'redis',
'STRIPE_': 'stripe',
'SENDGRID_': 'sendgrid',
'FEATURE_': 'features',
// Everything else goes to the root
};
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(env)) {
const prefix = Object.keys(prefixMap).find(p => key.startsWith(p));
if (prefix) {
const group = prefixMap[prefix];
const subKey = key.slice(prefix.length).toLowerCase();
if (!result[group]) result[group] = {};
(result[group] as Record<string, unknown>)[subKey] = value;
} else {
result[key.toLowerCase()] = value;
}
}
return result;
}
const structured = groupByPrefix(coerced);
console.log(JSON.stringify(structured, null, 2));
// {
// "port": 3000,
// "host": "0.0.0.0",
// "node_env": "development",
// "log_level": "debug",
// "database": {
// "host": "localhost",
// "port": 5432,
// "name": "appdb",
// "user": "postgres",
// "password": "local_dev_password",
// "pool_min": 2,
// "pool_max": 10,
// "ssl": false
// },
// "stripe": {
// "secret_key": "sk_test_4eC39HqLyjWDarjtT1zdp7dc",
// "webhook_secret": "whsec_abc123"
// },
// "features": {
// "new_checkout": true,
// "analytics": false
// }
// }
// dotenv-flow loads .env, then .env.local, then .env.{NODE_ENV}
// Later files override earlier ones — same cascade as Docker/Kubernetes env precedence
// File load order for NODE_ENV=staging:
// 1. .env (committed to git — defaults and structure)
// 2. .env.staging (committed to git — staging-specific non-secrets)
// 3. .env.staging.local (NOT committed — staging secrets for local developer)
// 4. .env.local (NOT committed — developer overrides for any env)
import dotenvFlow from 'dotenv-flow';
// Load for a specific environment
const config = dotenvFlow.config({ node_env: 'staging', silent: true });
const env = config.parsed ?? {};
// Export merged config to JSON (for debugging, CI comparison, etc.)
const exportConfig = (env: Record<string, string>, excludeSecrets = true) => {
const secretPatterns = [/password/i, /secret/i, /key/i, /token/i, /credential/i];
const isSecret = (key: string) => secretPatterns.some(p => p.test(key));
const safe = excludeSecrets
? Object.fromEntries(Object.entries(env).filter(([k]) => !isSecret(k)))
: env;
return JSON.stringify(groupByPrefix(
Object.fromEntries(Object.entries(safe).map(([k, v]) => [k, coerceValue(v)]))
), null, 2);
};
console.log(exportConfig(env, true)); // safe to log in CI — no secrets
// AWS Secrets Manager stores secrets as a JSON object
// Convert .env to the required format for bulk secret creation
import { SecretsManagerClient, CreateSecretCommand } from '@aws-sdk/client-secrets-manager';
function envToAwsSecretValue(env: Record<string, string>): string {
// AWS Secrets Manager expects a flat JSON object of string values
// (it doesn't support nested objects — the entire value is one JSON string)
return JSON.stringify(env);
}
// .env → single AWS secret (common pattern for 12-factor apps)
const secretValue = envToAwsSecretValue(env);
const client = new SecretsManagerClient({ region: 'us-east-1' });
await client.send(new CreateSecretCommand({
Name: 'myapp/production',
SecretString: secretValue, // {"DB_HOST":"db.internal","DB_PASSWORD":"..."}
}));
// Fetch and parse in your app:
// const secret = await getSecretValue('myapp/production');
// const env = JSON.parse(secret.SecretString);
// Object.assign(process.env, env); // inject into process.env
// Azure Key Vault pattern: each env var becomes a separate secret
// (Azure prefers individual secrets, not one JSON blob)
async function exportToAzureKeyVault(env: Record<string, string>, vaultUrl: string) {
const { SecretClient } = await import('@azure/keyvault-secrets');
const { DefaultAzureCredential } = await import('@azure/identity');
const client = new SecretClient(vaultUrl, new DefaultAzureCredential());
for (const [name, value] of Object.entries(env)) {
// Azure secret names: must match [a-zA-Z0-9-], replace _ with -
const secretName = name.toLowerCase().replace(/_/g, '-');
await client.setSecret(secretName, value);
}
}
.env.example with every variable defined but with empty or example values. The actual secret values live only in .env.local (gitignored) and in your secrets manager (AWS, Vault, GitHub Secrets). The .env.example doubles as documentation of what variables the application requires.process.exit(1) on validation failure. This surfaces missing or malformed environment variables immediately at deploy time, not hours later when the relevant code path runs.excludeSecrets filter pattern above to log your config structure (without secret values) in CI build logs. This makes it easy to diagnose configuration drift between environments without exposing sensitive values..env files that shouldn't exist in the container image. Configure dotenv to only load files when NODE_ENV !== 'production'.Q: Should I use dotenv in production or rely on system environment variables?
A: Rely on system environment variables in production. The 12-factor app methodology explicitly says config should come from the environment (set by your deployment platform), not from files checked into the repo. Dotenv is a development convenience — it simulates having those environment variables set without actually setting them globally on your machine.
Q: How do I handle multi-line values in .env files (like private keys)?
A: Wrap multi-line values in double quotes and use \n for newlines: PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEow...\n-----END RSA PRIVATE KEY-----". When dotenv parses this, it preserves the literal \n characters. Your application code must replace \\n with actual newlines when using the key: process.env.PRIVATE_KEY.replace(/\\n/g, '\n').
Q: How do I diff .env files across environments to find missing variables?
A: Export both environments to JSON using the grouping pattern above (with secrets masked), then diff the JSON files. Missing keys in one environment that exist in another are immediately visible. The env-diff npm package does this automatically for .env files.
Is the processing local-only?
Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.
Can I use this for enterprise projects?
Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.