Deep merge vs shallow merge JSON
A shallow merge combines only the first level of an object. A deep merge also visits nested objects and combines their fields. Neither phrase fully specifies array handling, conflict priority, or deletion semantics. Before combining configuration files, agree on those rules. A merge that is technically correct can still remove a setting your application needs.
Use the merge JSON files tool to compare results locally. Its object method is recursive, preserves non-conflicting fields, and records differences with the source filenames. You can choose earlier or later conflicting values and select how lists behave. The examples below isolate these decisions so you can recognize the result before downloading it.
The difference appears at a nested boundary
Imagine a base configuration with a service host and timeout, then an override that changes only the timeout. A shallow merge replaces the entire service object. The host disappears because it was inside the replaced value. A deep merge descends into service, keeps the host, and updates timeout. If the override instead replaces service with null, there are no two objects to recurse into; under a later-wins policy the entire value becomes null.
The following standalone Python block demonstrates both outcomes. Its deep function recursively combines dictionaries, replaces arrays and scalar values with the later value, and copies inputs so they are not mutated. It intentionally uses array replacement; the browser tool defaults to concatenation, so select Replace using winner rule to reproduce this function there.
from copy import deepcopy
def deep_merge(earlier, later):
if isinstance(earlier, dict) and isinstance(later, dict):
output = deepcopy(earlier)
for key, value in later.items():
if key in output:
output[key] = deep_merge(output[key], value)
else:
output[key] = deepcopy(value)
return output
return deepcopy(later)
base = {"service": {"host": "localhost", "timeout": 10}, "tags": ["base"]}
override = {"service": {"timeout": 30}, "tags": ["override"]}
shallow = {**base, **override}
deep = deep_merge(base, override)
assert shallow["service"] == {"timeout": 30}
assert deep["service"] == {"host": "localhost", "timeout": 30}
assert deep["tags"] == ["override"]
assert deep_merge({"service": {}}, {"service": None}) == {"service": None}
assert base["service"]["timeout"] == 10
print(deep)
Save the block as deep_merge.py and run it with Python 3. The embedded assertions document the
expected behavior as well as checking the implementation.
python3 deep_merge.py
Arrays require a separate policy
Arrays have positions, but positions are not necessarily identities. Pairing the first item in one export with the first item in another is usually unsafe. Common policies are to concatenate every item, concatenate and remove structurally equal duplicates, or replace the entire array. These policies produce different results even when object fields are merged recursively.
For example, combining [1, 2] with [2, 3] yields [1, 2, 2, 3] under concatenation and [1, 2, 3] under unique combination. Later replacement gives [2, 3]. Earlier replacement gives [1, 2]. Unique combination in this tool ignores object key order when comparing objects, but respects array order. It does not identify records by their ID. To reconcile customer or product records, use a keyed array join instead.
Earlier wins does not mean ignore later files
Earlier-wins is a conflict policy, not a stop-reading policy. A field that exists only in a later file is still added. When both values are objects, their non-overlapping nested fields are preserved. Only competing values use the earlier/later decision. Array concatenation and unique combination are independent of that winner switch; choose replacement if you want one whole list to win.
Null, false, zero, and an empty string are real values. They should not be treated as missing simply because they are false-like in a programming language. This tool detects whether a property exists rather than relying on truthiness. It also preserves a property named __proto__ as JSON data instead of treating it as an instruction to modify an object prototype.
Validate the result against your application
JSON syntax validation only proves that a parser can read a value. It does not prove that required fields exist, that a port is in range, or that a configuration combination makes sense. Run your application’s schema checks after merging. If you have no schema, at least compare a representative sample and review each conflict. The browser tool reports differences encountered while processing files in order, not a semantic explanation of whether each difference is safe.
Choose shallow merging when whole nested blocks are intentionally replaceable. Choose deep merging when independent nested settings should survive. Choose keyed joining when arrays represent entities with stable IDs. For a repeatable local pipeline, see merging JSON files with Python. For an immediate review with explicit options, return to the browser merger, add a small fixture, and check the conflict report before using the result elsewhere.
Use a decision table during review
Write a few expected outcomes before changing a merge implementation. Include an object versus a scalar, a scalar versus null, an empty list versus a populated list, and two nested objects with different fields. These boundaries reveal more than a large fixture full of non-conflicting values. In particular, an empty earlier list must remain empty under earlier-wins replacement; it is not an uninitialized accumulator.
| Boundary | Decision to document |
|---|---|
| Objects | Recurse into fields or replace the whole block? |
| Lists | Concatenate, structurally deduplicate, or replace? |
| Null | Keep as a value or use a separate deletion protocol? |
Do not infer deletion from an absent field. Absence generally means the later source has no opinion about that field. If your application requires removal, define a separate, unambiguous operation and validate it before applying the result. This tool does not implement JSON Patch or JSON Merge Patch and should not be substituted for either protocol without examining the differences.
When a merged object will become executable configuration, also restrict permitted keys and value ranges. Safe JSON property copying prevents one class of JavaScript prototype problem, but it does not make arbitrary configuration safe to load into every application.
When not to use this approach
Do not use a generic deep merge as an implementation of JSON Patch or JSON Merge Patch. Their operation and deletion rules differ. Recursive merging is also inappropriate when a nested block must be replaced atomically or arrays contain entities that need an ID-based join. A valid merged configuration can still violate your application's schema. Use explicit operations, validate the allowed keys, and test the resulting configuration before loading it into a production application.