How to Deep Merge JSON Objects in JavaScript and Python
The built-in ways to combine objects lose nested settings. Here is a short, tested deep merge for JavaScript and Python, and how to handle arrays, null and __proto__.
To deep merge JSON objects, you need a few lines of your own code. Merging a defaults object with user settings, or a base config with an environment override, is one of the most common things done with JSON in code. The built-in tools in both JavaScript and Python only merge the top level, and they lose nested values without any warning.
The JSON Merge tool merges documents in the browser, and its page explains the difference between shallow and deep merging. This guide shows how to do it in your own code, with a short function for each language.
Spread and dict union are shallow
const defaults = { db: { host: "localhost", port: 5432 }, tags: ["a"] };
const overrides = { db: { host: "db.internal" }, tags: ["b"] };
{ ...defaults, ...overrides }
// { db: { host: "db.internal" }, tags: ["b"] } port is goneThe override's db object replaced the default db object entirely, so port disappeared. Python's dictionary union, defaults | overrides, and dict.update behave the same way. For flat settings that is fine. For nested config it silently drops values.
Deep merge JSON objects in JavaScript
const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
function deepMerge(target, source) {
const result = { ...target };
for (const [key, value] of Object.entries(source)) {
if (key === "__proto__") continue;
result[key] =
isPlainObject(result[key]) && isPlainObject(value) ? deepMerge(result[key], value) : value;
}
return result;
}
deepMerge(defaults, overrides);
// { db: { host: "db.internal", port: 5432 }, tags: ["b"] }When both sides hold an object, it recurses. Otherwise the override wins. It returns a new object and leaves defaults unchanged, which matters when the same defaults are reused for every request.
Deep merge nested dicts in Python
def deep_merge(base, override):
result = dict(base)
for key, value in override.items():
if isinstance(result.get(key), dict) and isinstance(value, dict):
result[key] = deep_merge(result[key], value)
else:
result[key] = value
return result
deep_merge(defaults, overrides)
# {'db': {'host': 'db.internal', 'port': 5432}, 'tags': ['b']}Three decisions every deep merge makes
Arrays: replace, append or match
Both functions above replace arrays, so tags became ["b"]. That is the safest default, because nobody is surprised by it. Appending is right for lists that only grow, such as allowed origins. For lists of objects, merge items that share an ID:
function mergeById(base, override) {
const byId = new Map(base.map((item) => [item.id, item]));
for (const item of override) byId.set(item.id, deepMerge(byId.get(item.id) ?? {}, item));
return [...byId.values()];
}
mergeById([{ id: 1, qty: 1, sku: "A" }, { id: 2, qty: 5 }], [{ id: 1, qty: 3 }, { id: 3, qty: 1 }]);
// [{ id: 1, qty: 3, sku: "A" }, { id: 2, qty: 5 }, { id: 3, qty: 1 }]Watch out for libraries that merge arrays by position. lodash's merge combines ["a", "b", "c"] with ["z"] into ["z", "b", "c"], so an override can never shorten a list.
null: store it or delete the key
In the functions above, an override of null stores null. Some formats use null to mean remove this key instead, JSON Merge Patch for example. If you want that, add one line: when value === null, delete the key from the result and continue. Decide once and document it, because a caller cannot guess.
Unsafe keys
JSON.parse turns a key named __proto__ into an ordinary property, and a careless merge then writes it through to the object's prototype. The continue line in the JavaScript function skips it. Always keep that guard when merging JSON that came from outside your code, such as a request body or a user's settings file.
Related tools
Compare two JSON documents and see a precise structural diff.
Recursively strip every null value from a JSON document.
Pretty-print JSON with configurable indentation and instant validation.
Recursively sort object keys alphabetically for stable, diffable JSON.
Related guides
JSON.stringify(a) === JSON.stringify(b) fails as soon as key order differs. Here is how to check equality properly in JavaScript and Python, and how to list exactly what changed.
A JSON.stringify replacer removes most nulls but not all of them. Here is a tested recursive function for JavaScript and Python, and the one case where removing nulls breaks things.
Two formatters can disagree about the same JSON file. Here is how a team picks one, applies it in every editor and before every commit, and makes CI enforce it.
The same data can be written with its keys in a different order in each language, and sometimes on each run. Here is where the order comes from, and the fix for each case.