How to apply environment variable overrides to a json config
- Step 1Paste the base config into JSON Input 1 — Input 1 is your
config.base.json— the all-environments defaults. It must be a single valid JSON object. This is the layer the environment override folds into. - Step 2Paste the environment override into JSON Input 2 — Input 2 is
config.<env>.json— the partial that restates only the keys that differ for this environment. Last-wins means anything here overrides the base for the same key path. - Step 3If you have a .env file, convert it first — This tool merges JSON objects, not
KEY=valueenv files. Turn.envlines into a JSON object ({ "DB_HOST": "...", "LOG_LEVEL": "warn" }) before pasting. If your base config nests values (db.host), shape the override to match that nesting so deep merge targets the right leaf. - Step 4Keep Deep merge selected — Deep merge (default) folds nested config sections recursively so a partial override doesn't wipe a whole section. Use Array append if a list-valued setting (e.g.
allowedOrigins) should accumulate base + override entries instead of being replaced. - Step 5Click Merge Objects — Both inputs are parsed and folded base-then-override. If either isn't valid JSON the merge stops with the parser error. At least 2 non-empty inputs are required.
- Step 6Verify the resolved config, then export — Check that the environment-specific values won and the base values you expected survived. Copy or Download (
merged.json). To audit exactly what changed, diff the base against the merged output with the json-diff.
Strategy behaviour for env overrides
How each strategy resolves a base-vs-override collision. 'Section' = both sides are nested config objects; 'List' = both are arrays; 'Scalar' = string/number/boolean/null or mismatched types. Base is Input 1; the override (Input 2) is 'later'.
| Strategy | Config section (object) | List value (array) | Scalar value | When to use |
|---|---|---|---|---|
| Deep merge (default) | Recursively merged | Override replaces base list | Override wins | standard partial env override |
| Shallow | Override replaces whole section | Override replaces base list | Override wins | flat single-level config |
| First wins | Base section kept (override ignored) | Base list kept | Base kept | override only fills missing keys |
| Array append | Override replaces whole section | base ++ override | Override wins | lists that should accumulate |
Common env-override scenarios
Typical base→override situations and how deep merge resolves each leaf.
| Key | Base value | Prod override | Resolved (deep merge) |
|---|---|---|---|
db.host | localhost | prod.internal | prod.internal (override) |
db.port | 5432 | (absent) | 5432 (base preserved) |
log.level | debug | warn | warn (override) |
cache | { ttl: 60 } | (absent) | { ttl: 60 } (base preserved) |
featureX.enabled | false | true | true (override) |
Cookbook
Real base/override pairs from environment-config resolution, showing what deep merge produces. All runs are client-side.
Production override changes only what differs
ExampleThe prod override restates just the DB host and log level. Deep merge applies them while keeping every other base key — port, pool, cache — intact.
Input 1 (config.base.json):
{ "db": { "host": "localhost", "port": 5432, "pool": 10 },
"log": { "level": "debug" }, "cache": { "ttl": 60 } }
Input 2 (config.production.json):
{ "db": { "host": "prod.internal" }, "log": { "level": "warn" } }
Strategy: Deep merge
Output:
{
"db": { "host": "prod.internal", "port": 5432, "pool": 10 },
"log": { "level": "warn" },
"cache": { "ttl": 60 }
}Why shallow loses keys (the bug deep merge avoids)
ExampleSame inputs, Shallow strategy. Shallow replaces the whole db and log objects, so port, pool, and any other db key vanish. Use this only when you genuinely want to swap whole sections.
Input 1: { "db": { "host": "localhost", "port": 5432, "pool": 10 } }
Input 2: { "db": { "host": "prod.internal" } }
Strategy: Shallow
Output:
{ "db": { "host": "prod.internal" } }
→ port and pool GONE. Deep merge would keep them.Convert a .env file to an override object first
ExampleThe tool merges JSON, not KEY=value lines. Reshape your .env into a JSON object matching the base's nesting, then merge. Here the .env keys map onto nested config leaves.
.env (NOT directly usable):
DB_HOST=prod.internal
LOG_LEVEL=warn
Reshape to JSON Input 2 (match base nesting):
{ "db": { "host": "prod.internal" }, "log": { "level": "warn" } }
Merge with base → resolved config.
(If base uses flat keys like "DB_HOST", keep the override flat too.)Accumulate CORS origins instead of replacing
ExampleProduction should add origins to the base list, not replace it. Deep merge would overwrite the array; Array append concatenates base and override entries.
Input 1 (base): { "allowedOrigins": ["http://localhost:3000"] }
Input 2 (prod): { "allowedOrigins": ["https://app.example.com"] }
Strategy: Deep merge → { "allowedOrigins": ["https://app.example.com"] }
Strategy: Array append → { "allowedOrigins":
["http://localhost:3000", "https://app.example.com"] }Audit exactly what the override changed
ExampleAfter resolving, diff the base against the merged config to produce a precise change list — confirmation that prod touched only the keys you intended.
Step 1 — merge base + prod override here → resolved.json Step 2 — paste config.base.json and resolved.json into json-diff json-diff output (illustrative): ~ db.host: localhost → prod.internal ~ log.level: debug → warn → exactly two keys changed; everything else preserved.
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.
Tool merges JSON objects, not KEY=value .env files
Wrong input formatPasting raw .env lines (DB_HOST=prod.internal) is not valid JSON and the parser rejects it. Convert env files to a JSON object first, shaping the keys to match your base config's nesting. If your base uses nested sections, the override must nest the same way for deep merge to hit the right leaf.
Override array replaces the base array
By designIn deep merge, a list-valued setting in the override replaces the base list entirely — it doesn't append. A prod allowedOrigins: ["https://app..."] will drop the base's localhost entry. Use Array append if origins/allowlists should accumulate, and always check array-valued keys after merging.
Override section shape doesn't match the base
Type mismatchIf the base nests a value ({ "db": { "host": ... } }) but your override flattens it ({ "db_host": ... }), deep merge can't fold them — db_host becomes a separate top-level key and db.host keeps the base value. Match the override's nesting to the base, or unflatten with the json-unflattener first.
An input isn't valid JSON
Parse errorTrailing commas, comments, or single quotes (common when copying from a .jsonc or YAML config) cause JSON.parse to fail and the merge stops. Strip comments, or convert YAML config with the yaml-to-json tool, then merge the resulting JSON objects.
Override sets a section to null to disable it
Last winsAn override with { "cache": null } replaces the whole base cache object with null under last-wins (object-vs-null type mismatch). It does not delete the key. If your loader treats null as 'disabled', that's fine; to remove null keys from the resolved config, run the json-null-stripper.
More than 2 environment layers on free tier
Upgrade requiredbase + region-default + prod is three inputs, exceeding the free 2-input limit. Pro allows up to 10. On free, merge base + region first, copy the result back into Input 1, then add the prod override as Input 2 for a second pass.
Secret value present in the override
PreservedSecrets like connection strings are merged as plain string values and appear verbatim in the output — the tool doesn't mask them, but it also never uploads them (everything runs in your browser). If you need to redact secrets before sharing a resolved config, use the json-anonymizer afterward.
Numbers represented as strings
PreservedIf the base has "port": 5432 (number) and the override has "port": "5432" (string from a stringly-typed env source), the override's string replaces the number under last-wins — types aren't coerced. Keep the override's value types consistent with the base, or your config loader may receive a string where it expects a number.
Frequently asked questions
Can I paste a .env file directly?
No — this tool merges JSON objects, not KEY=value env files. Convert your .env to a JSON object first, shaping the keys to match your base config's structure (nested or flat). Once both inputs are JSON objects, the merge applies the override on top of the base with last-wins precedence.
Why does the environment override win conflicts?
Inputs merge left-to-right and the last input wins. With the base in Input 1 and the environment override in Input 2, the override is 'later' and therefore wins any key it restates. That matches a normal config loader where the environment layer takes precedence over defaults.
Will a partial override drop my untouched base keys?
Not with Deep merge — it folds each section recursively, so an override that restates only db.host keeps db.port, db.pool, and every other base key. Keys are only lost with Shallow (which replaces whole sections) or when an override sets a parent to a non-object value.
How do I combine list values from base and override?
Use the Array append strategy. By default (deep merge) an override array replaces the base array; array-append concatenates them, which is what you want for allowlists, CORS origins, or plugin lists that should accumulate across layers.
Can I resolve base + region + environment in one pass?
On Pro, yes — up to 10 inputs, folded left-to-right (least-specific first, most-specific last). On the free tier you can merge 2 at a time: combine base + region, copy the result back as the base, then add the environment override for a second pass.
Are production secrets uploaded anywhere?
No. Parsing and merging happen entirely in your browser. Connection strings, API keys, and any other secret in your override are processed in-tab and never sent to a server. Only an anonymous run count is recorded for signed-in dashboard stats.
My YAML config won't paste — what do I do?
This tool only accepts JSON. Convert your YAML config to JSON first with the yaml-to-json tool, then merge the resulting JSON objects. After merging you can convert back with the json-to-yaml tool if your deployment expects YAML.
How do I confirm the override only changed what I intended?
Merge here, copy the resolved config, then drop the base and the resolved output into the json-diff. The diff lists every changed key, so you can verify the environment override touched only the leaves you meant to override and nothing else.
What if my override's value types differ from the base?
Types aren't coerced — a string override replaces a number under last-wins, and vice versa. If your env source produces stringly-typed values ("true", "5432") but the base uses real booleans/numbers, normalise the override's types before merging so your config loader gets what it expects.
Does it support deleting a key via the override?
No — there's no delete semantic. An override can replace a value (including with null) but can't remove a key from the result. If you need to strip keys, post-process: use the json-key-filter to drop unwanted keys or the json-null-stripper to remove null-valued ones.
Can I change the output formatting?
The web tool outputs 2-space indentation with no control to change it. Re-format the resolved config with the json-prettifier (tabs/4-space) or compact it with the json-minifier for embedding.
Can I automate env resolution in a pipeline?
Yes — the merger runs as a runner-local tool on Pro, keeping secrets on your machine, with the same deep/shallow/first-wins/array-append strategies. A deploy pipeline can fold base + env JSON into a resolved config per environment and validate it with the json-validator before shipping.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.