How to unflatten environment variable names into a json config tree
- Step 1List the variables as a flat JSON object — Turn your
.envor variable list into a JSON object:{ "DB_HOST": "localhost", "DB_PORT": "5432", "REDIS_URL": "redis://..." }. One flat object per run. - Step 2Strip the common app prefix first — If every key starts with
APP_orSERVICE_, remove that prefix with the JSON Key Renamer so the prefix does not become a useless top-level node before you unflatten. - Step 3Set the Key delimiter to underscore — Type
_into the single Key delimiter field. If you adopted a double-underscore convention to protect compound names, type__instead (up to 3 characters allowed). - Step 4Drop or paste and run — Drop the
.jsonfile (free tier up to 2 MB) and click Unflatten JSON. Everything runs locally — secrets never reach a server. - Step 5Cast types in your loader, not here — Ports, timeouts, and feature flags come out as strings. Cast them (
Number(cfg.db.port),cfg.debug === 'true') in your config loader or coerce them in your Zod schema withz.coerce. - Step 6Generate the schema or interface — Use the nested output as the reference shape to write a Zod/Joi schema or a TypeScript
Configinterface, then validate the real environment against it at startup.
Variable naming and what unflattening produces
Behaviour with the underscore delimiter. Compound names split on every underscore, which is the main thing to plan for.
| Flat variable | Output (delimiter _) | Comment |
|---|---|---|
"DB_HOST": "localhost" | { db: { host: "localhost" } } | Clean two-level nesting |
"DB_PORT": "5432" | { db: { port: "5432" } } | Value stays a string — cast in your loader |
"REDIS_URL": "redis://x:6379" | { redis: { url: "redis://x:6379" } } | Connection string preserved, :// untouched |
"S3_BUCKET_NAME": "prod" | { s3: { bucket: { name: "prod" } } } | Over-splits a compound name — use __ or rename |
"FEATURE_X": "true" | { feature: { x: "true" } } | Boolean stays a string |
Strategies for compound names and prefixes
Practical patterns for env-to-config work given that the tool splits on one delimiter and never casts types.
| Problem | Approach | Tool |
|---|---|---|
Common prefix APP_ clutters the tree | Strip the prefix before unflattening | JSON Key Renamer |
Compound name BUCKET_NAME over-splits | Adopt double-underscore nesting (S3__BUCKET_NAME) and set delimiter __ | Key delimiter field |
| Ports/flags arrive as strings | Coerce in your schema (z.coerce.number()) | Zod / Joi in your code |
| Secrets must not leave the machine | Run locally; nothing is uploaded | Built-in (client-side) |
| Need a typed interface from the shape | Feed the nested output to the type generator | JSON to TypeScript |
Cookbook
Real before/after config reconstructions. Secrets shown are placeholders; the delimiter is what you would type in the Key delimiter field.
Database and Redis config from underscore variables
ExampleA standard twelve-factor set of variables rebuilds into domain groups you can destructure.
Key delimiter: _
Flat input:
{
"DB_HOST": "localhost",
"DB_PORT": "5432",
"REDIS_URL": "redis://localhost:6379"
}
Output:
{
"db": { "host": "localhost", "port": "5432" },
"redis": { "url": "redis://localhost:6379" }
}Strip the APP_ prefix before unflattening
ExampleWithout stripping, APP_ becomes a single top-level node wrapping everything. Rename it away first.
Raw: { "APP_DB_HOST": "localhost", "APP_DB_PORT": "5432" }
Step 1 — JSON Key Renamer: remove prefix APP_
→ { "DB_HOST": "localhost", "DB_PORT": "5432" }
Step 2 — Unflatten (delimiter _):
→ { "db": { "host": "localhost", "port": "5432" } }Compound names over-split — use double underscore
ExampleS3_BUCKET_NAME splits into three levels. Switch the convention to __ for nesting boundaries to keep BUCKET_NAME together.
Delimiter _ : "S3_BUCKET_NAME": "prod"
→ { "s3": { "bucket": { "name": "prod" } } } (probably wrong)
Rename to S3__BUCKET_NAME, delimiter __ :
→ { "s3": { "BUCKET_NAME": "prod" } } (intended)Values stay strings — coerce in your schema
ExamplePort and flag values are not cast. Validate and coerce them in Zod at startup.
Output: { "db": { "port": "5432" }, "feature": { "x": "true" } }
const schema = z.object({
db: z.object({ port: z.coerce.number() }),
feature: z.object({ x: z.coerce.boolean() }),
});
const cfg = schema.parse(unflattened);Generate a typed config interface
ExampleFeed the nested config shape into the type generator to scaffold a TypeScript interface for your loader.
Nested config (the .nested.json output)
│
▼ JSON to TypeScript
interface Config {
db: { host: string; port: string };
redis: { url: string };
}Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
Compound variable name (S3_BUCKET_NAME) with delimiter _
Over-splitsSplits into { s3: { bucket: { name } } }. Adopt a double-underscore convention for nesting boundaries and set the delimiter to __, or rename keys first with the JSON Key Renamer.
Port or flag value ("5432", "true")
Preserved as stringNo type casting happens. Coerce values in your config loader or schema (e.g. z.coerce.number()). The JSON Format Fixer repairs syntax, not value types.
Connection string with :// and @ in the value
PreservedOnly keys are split, never values. postgres://user:pass@host:5432/db is kept intact under the right key.
Common prefix (APP_) not stripped
Extra top nodeEvery variable nests under one app node. Strip the prefix first with the JSON Key Renamer for a clean tree.
Short key collides with a longer nested path
Last write winsIf both DB and DB_HOST exist, the later key determines the result. Rare in env sets, but reorder or rename if it occurs.
Numeric segment (SERVERS_0_HOST)
Object, not arrayBecomes { servers: { "0": { host } } }, a map keyed "0", not an array. Convert to an array if your config expects a list of servers.
Input is an array of env objects
Not the expected inputUnflatten one flat object per run. Combine multiple environments into separate runs.
Malformed JSON pasted from a .env dump
Parse errorThe input must be valid JSON, not raw .env lines. Convert .env to a JSON object first, then fix any syntax with the JSON Format Fixer.
Config file over the free 2 MB limit
Blocked on freeUnusual for config, but the free JSON tier caps files at 2 MB. Upgrade to Pro to remove the limit.
Frequently asked questions
Does the tool convert PORT=5432 into a real number?
No. Environment values are preserved as strings, so "5432" stays the string "5432" and "true" stays a string. Coerce types in your config loader or schema (for example z.coerce.number()); the JSON Format Fixer fixes JSON syntax but does not cast value types.
How do I handle prefixes like APP_ or SERVICE_ that should not become nesting levels?
Strip the prefix before unflattening with the JSON Key Renamer. Otherwise every variable nests under a single app node, which is rarely what you want.
What about variables with underscores that are not separators, like S3_BUCKET_NAME?
With the underscore delimiter, S3_BUCKET_NAME becomes { s3: { bucket: { name } } }. To keep compound names together, adopt a double-underscore convention for nesting boundaries (S3__BUCKET_NAME) and set the delimiter to __, or rename the keys first.
Are environment values, including connection strings, sent to JAD Apps?
No. Unflattening runs entirely in your browser. Database connection strings, API keys, and secrets are never uploaded to JAD Apps servers.
Can I paste my .env file directly?
No. The input must be a valid JSON object of variable names and values, not raw KEY=value lines. Convert the .env to a JSON object first, then unflatten.
Which delimiter should I use for env vars?
Type _ for the conventional underscore naming. If you adopted double underscores to protect compound names, type __ — the Key delimiter field accepts up to 3 characters.
How are list-style variables like SERVERS_0_HOST handled?
They rebuild as objects keyed by the numeric segment — { servers: { "0": { host } } } — not as an array. Convert that to an array if your config expects a list.
Can I generate a TypeScript type from the result?
Yes. Feed the nested config output into JSON to TypeScript to scaffold a Config interface, or JSON to Zod for a runtime schema.
Can I change the output indentation?
No, the output is always 2-space indented. Run your formatter over the saved file if you need a different style.
What happens if two variables collide on a path?
Last write wins — a short key like DB is overwritten when DB_HOST arrives later (and vice versa). This is deterministic for a given order; reorder or rename to control the result.
What is the file-size limit?
2 MB per file on the free JSON tier. Pro removes the limit. The unflattener is a Pro tool.
How do I flatten a nested config back to env-style keys?
Use the JSON Flattener with the underscore delimiter to produce flat DB_HOST-style keys from a nested config object.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.