How to deep-merge multiple json configuration files
- Step 1Paste your base config into JSON Input 1 — Input 1 is the base layer — its values are the starting point that later inputs override. Paste your
config.base.json(ordefaults.json) verbatim. It must be a single valid JSON object ({ ... }); the merger iterates object keys, so a top-level array behaves differently (see edge cases). - Step 2Paste the environment override into JSON Input 2 — Input 2 is the override layer — for a single base+env merge this is
config.production.json. Because inputs merge left-to-right with last-wins, anything in Input 2 takes precedence over Input 1 for conflicting keys. - Step 3Add more layers with Add input — Need base + env + local? Click Add input to append another textarea. The free tier allows 2 inputs total; Pro raises this to 10. Each input is capped at 2 MB on the free tier. Order is precedence: later inputs win.
- Step 4Choose the merge strategy — Pick Deep merge (the default) so nested config sections fold recursively. The other radio options are Shallow (top-level last-wins, no recursion), First wins (existing keys are never overwritten — good for 'defaults fill the gaps' merges), and Array append (concatenate arrays at matching keys instead of replacing them).
- Step 5Click Merge Objects — The tool parses every input with
JSON.parse, folds them in order, and renders the result. If any input is not valid JSON it stops and shows the parser message — fix the offending input and re-run. You need at least 2 non-empty inputs. - Step 6Verify, then copy or download — Check the stat line (inputs merged · keys · size) and scan the output for the values you expected to override. Use Copy to grab the JSON or Download to save
merged.json. To confirm the result parses cleanly elsewhere, follow up with the json-validator.
Merge strategy behaviour for config files
How each of the four strategies resolves a key collision. 'Object' means both sides are plain JSON objects; 'Array' means both sides are arrays; 'Primitive' means a string/number/boolean/null or mismatched types. Inputs are folded left-to-right, so 'later wins' refers to a key defined in a more-rightward input.
| Strategy | Nested object collision | Array collision | Primitive / type-mismatch collision | Best for |
|---|---|---|---|---|
| Deep merge (default) | Recursively merged key-by-key | Later array replaces earlier (no concat) | Later value wins | base → env → local layered config |
| Shallow | Whole later object replaces earlier | Later array replaces earlier | Later value wins | flat single-level config where you want a clean section swap |
| First wins | Key skipped if already present (later object dropped, no recursion) | Skipped if key already present | Earlier value kept | filling missing keys without ever overriding a set value |
| Array append | Whole later object replaces earlier (objects are NOT deep-merged in this mode) | Concatenated: earlier ++ later | Later value wins | configs whose arrays (allowlists, plugins) should accumulate |
Input limits and behaviour
Real limits read from the merger implementation and tier-limits config. The web tool fixes output indentation at 2 spaces.
| Aspect | Free tier | Pro tier | Notes |
|---|---|---|---|
| JSON inputs per merge | 2 | 10 | Minimum 2 non-empty inputs required to run |
| Max size per input | 2 MB | Larger | Each textarea is checked independently |
| Output indentation | 2 spaces | 2 spaces | Web UI has no indent control; pretty-printed at 2 |
| Merge order | left-to-right | left-to-right | Input 1 is the base; the last input wins conflicts |
| Processing location | your browser | your browser | No upload; runs in-tab via JSON.parse + recursive merge |
Cookbook
Real base/override pairs from layered configuration setups, showing exactly what each strategy produces. All run client-side.
Base + production override keeps untouched keys
ExampleThe classic reason to deep-merge instead of spreading: the production override only changes the DB host, but a shallow spread would wipe out port and pool. Deep merge folds the db object key-by-key.
Input 1 (config.base.json):
{ "db": { "host": "localhost", "port": 5432, "pool": 10 }, "debug": true }
Input 2 (config.production.json):
{ "db": { "host": "prod.internal" }, "debug": false }
Strategy: Deep merge
Output (merged.json):
{
"db": {
"host": "prod.internal",
"port": 5432,
"pool": 10
},
"debug": false
}Shallow vs deep on the same inputs
ExampleSame two inputs as above, but with the Shallow strategy. Shallow replaces the whole db object with the override's version, so port and pool vanish. This is the bug deep merge exists to prevent — useful to see side by side.
Input 1: { "db": { "host": "localhost", "port": 5432, "pool": 10 }, "debug": true }
Input 2: { "db": { "host": "prod.internal" }, "debug": false }
Strategy: Shallow
Output:
{
"db": { "host": "prod.internal" },
"debug": false
}
→ port and pool are GONE. Use Deep merge to keep them.Three layers: base → env → local
ExampleAdd a third input for a developer's local override. Inputs fold left-to-right, so the local file (Input 3) wins, then env (Input 2), then base (Input 1). Requires Pro for >2 inputs, or the orchestrator path.
Input 1 (base): { "log": { "level": "info" }, "port": 3000 }
Input 2 (env): { "log": { "level": "warn" } }
Input 3 (local): { "log": { "json": true }, "port": 8080 }
Strategy: Deep merge
Output:
{
"log": { "level": "warn", "json": true },
"port": 8080
}
→ level from env, json from local, port from local.Array append for accumulating plugin lists
ExampleWhen a config section is a list that should grow across layers — enabled plugins, IP allowlists — Deep merge would replace the array. Array append concatenates instead.
Input 1: { "plugins": ["core", "auth"] }
Input 2: { "plugins": ["metrics"] }
Strategy: Deep merge → { "plugins": ["metrics"] } (replaced)
Strategy: Array append → { "plugins": ["core", "auth", "metrics"] }
Note: array-append does NOT deep-merge nested objects —
objects are replaced last-wins in this mode.First-wins to fill gaps without overriding
ExampleSometimes you want the base to be authoritative and a secondary file only to supply keys the base is missing. First wins keeps every key already present and only adds new ones.
Input 1 (authoritative): { "region": "us-east-1", "retries": 3 }
Input 2 (fallback): { "region": "eu-west-1", "timeout": 30 }
Strategy: First wins
Output:
{
"region": "us-east-1",
"retries": 3,
"timeout": 30
}
→ region NOT overridden; timeout added.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.
Deep merge replaces arrays instead of combining them
By designIn the default deep strategy, when both sides have an array at the same key, the later array replaces the earlier one — they are not concatenated or merged element-by-element. If you expected ["a"] + ["b"] to become ["a","b"], switch to the Array append strategy. This is the single most common surprise with config merges, so verify array-valued keys after merging.
An input is not valid JSON
Parse errorEach input is parsed with JSON.parse after trimming. A trailing comma, a comment, a single-quoted string, or an unquoted key throws and the merge stops with the parser's message. JSON has no comments, so strip // lines from any config.jsonc-style file first. To repair common syntax slips automatically, run the source through the json-format-fixer before merging.
Fewer than 2 non-empty inputs
RejectedThe tool needs at least 2 inputs that contain text after trimming. Empty textareas are ignored. If you only have one config object and just want to pretty-print or validate it, use the json-prettifier or json-validator instead.
More inputs than the tier allows
Upgrade requiredThe free tier merges 2 inputs; Pro raises the cap to 10. Adding a third input on free shows an upgrade prompt rather than merging. For a base/env/local three-layer merge you need Pro, or you can merge base+env first, copy the result, and paste it back as Input 1 with local as Input 2.
Input larger than 2 MB on the free tier
Size limitEach input is limited to 2 MB on the free tier; an oversized input triggers an upgrade prompt before merging. Config files are usually far smaller than this, so hitting the limit often means you pasted an entire data dump rather than a config object — double-check the input.
Top-level value is an array, not an object
Index mergeThe merger treats every input as an object and iterates its keys. A top-level JSON array ([1,2,3]) has numeric keys 0,1,2, so two arrays merge by index (position 0 with position 0, etc.) rather than concatenating. For combining JSON arrays as data records, this tool is the wrong fit — config files are objects. Confirm your top level is { ... }.
A key holds null in the override
Last winsnull is a primitive here. If the base has { "proxy": { "url": "..." } } and the override has { "proxy": null }, the override's null replaces the whole object (object-vs-null is a type mismatch, so it takes the last-wins branch). To strip nulls from the result afterward, use the json-null-stripper.
Duplicate keys within a single input
JSON.parse ruleIf one input object literally contains the same key twice, JSON.parse keeps the last occurrence (standard JS behaviour) before merging even begins. This happens before any strategy applies, so the merger never sees the earlier value. Clean duplicate keys in the source file if both mattered.
Output indentation is fixed at 2 spaces
ExpectedThe web tool pretty-prints the merged result at 2-space indentation with no UI control to change it. If you need tabs, 4-space indent, or minified output, run the merged JSON through the json-prettifier or json-minifier afterward.
Frequently asked questions
What does deep merge mean for JSON config files?
Deep merge recursively combines nested objects key-by-key. If your base has { "db": { "host": "a", "port": 5432 } } and your override has { "db": { "host": "b" } }, deep merge produces { "db": { "host": "b", "port": 5432 } } — the override's host wins, but the base's port survives. A shallow spread ({...base, ...override}) would replace the entire db object and lose port, which is exactly the failure deep merge prevents.
Which input takes precedence when keys conflict?
Inputs are merged left-to-right, so Input 1 is the base and the last input wins. Put your most-authoritative override last. The exception is the First wins strategy, which inverts this: the earliest occurrence of a key is kept and later ones are ignored.
Does deep merge combine arrays?
No — in the default deep strategy, arrays are replaced by the later value, not concatenated. This is the most common point of confusion. If you want arrays at matching keys to be combined (["a"] + ["b"] → ["a","b"]), select the Array append strategy instead.
How many config files can I merge at once?
Two on the free tier, up to ten on Pro. You need at least 2 non-empty inputs to run a merge. For a base + environment + local three-layer setup, you'll need Pro, or you can merge two layers, copy the result, and paste it back as the base for a second pass.
Is my config uploaded anywhere?
No. Parsing and merging happen entirely in your browser tab using the page's own JavaScript. Connection strings, API keys, and other secrets in your config never leave your machine. Only an anonymous run counter is recorded for signed-in dashboard stats.
Can I change the output indentation?
Not in the web tool — output is pretty-printed at 2 spaces with no UI control for it. The underlying merge supports 0/2/4-space output, but to get tabs, 4-space, or minified output here, pass the merged JSON through the json-prettifier or json-minifier.
What happens if one of my files has a syntax error?
The merge stops and shows the JSON parser's error message for the offending input. JSON is strict: no comments, no trailing commas, no single quotes, keys must be double-quoted. If your config uses JSONC-style comments, strip them first; for accidental syntax slips, the json-format-fixer can repair many of them automatically.
Why did a nested key disappear after merging?
Almost always because you used Shallow instead of Deep merge — Shallow replaces a whole object value instead of folding it. Re-run with Deep merge. The other cause is an override that set the parent to null or a non-object, which replaces the whole branch under last-wins.
Can I keep a base value and only fill in missing keys?
Yes — use the First wins strategy. It keeps every key already present (from earlier inputs) and only adds keys that don't yet exist, so your authoritative base is never overridden while a secondary file supplies the gaps.
Does this validate my config against a schema?
No, it only merges. To validate the merged result against a JSON Schema or check it's well-formed, use the json-validator. To generate a schema from a sample config, see the json-schema-generator.
How do I compare the merged config against the original base?
Merge here, copy the result, then drop both the base and the merged output into the json-diff to see exactly which keys the override changed. That's a reliable way to confirm an environment override touched only what you intended.
Can I run this merge in an automated pipeline?
Yes. The merger is available as a runner-local tool (Pro tier) so config secrets stay on your machine — the orchestrator's json[] port feeds multiple inputs and the same deep/shallow/first-wins/array-append strategies apply. For a one-off, the browser tool is the fastest path.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.