How to deep merge objects and arrays in JavaScript using Lodash _.merge(). Copy-paste examples for nested objects, array of objects, two objects merge, and custom merge behavior with _.mergeWith().

Lodash _.merge() is a JavaScript utility method that recursively deep merges properties of source objects into a destination object. Unlike Object.assign() or the spread operator (...), _.merge() handles nested objects and arrays without overwriting inner properties — making it ideal for merging configuration objects, API responses, and complex state updates.
// Quick syntax
_.merge(destinationObject, sourceObject1, sourceObject2, ...)
Here is a quick example that shows how _.merge() works:
const defaults = { theme: "light", editor: { fontSize: 14, lineNumbers: true } };
const userPrefs = { editor: { fontSize: 18 } };
const config = _.merge(defaults, userPrefs);
// Result: { theme: "light", editor: { fontSize: 18, lineNumbers: true } }
Notice how editor.lineNumbers is preserved. With Object.assign() or spread, the entire editor object would be replaced, and lineNumbers would be lost.
In this guide, you will learn how _.merge() works, how to deep merge objects and arrays, how to customize merge behavior with _.mergeWith(), and when to use alternatives like Object.assign or the spread operator instead.
🔍 Working with Lodash? Also check out our guides on Lodash _.sortBy() for sorting arrays and objects, and Lodash _.groupBy() for grouping data by key.
Before using _.merge(), you need Lodash in your project. Here are the most common ways to set it up:
Install the full Lodash library:
npm install lodash
Import _.merge() in your code:
// ES6 Module — import only merge (tree-shakable)
import merge from 'lodash/merge';
// CommonJS
const merge = require('lodash/merge');
// Or import the full library
import _ from 'lodash';
// then use _.merge()
Lightweight alternative — install only the merge module:
npm install lodash.merge
import merge from 'lodash.merge';
> Tip: Importing lodash/merge instead of the full lodash library can reduce your bundle size significantly. The full Lodash library is ~70KB (minified + gzipped), while lodash.merge alone is ~6KB.
Here is the complete syntax for _.merge():
_.merge(object, [sources])
| Parameter | Type | Description |
|---|---|---|
object |
Object | The destination object. This object is mutated directly. |
[sources] |
...Object | One or more source objects to merge into the destination. |
| Returns | Object | Returns the modified destination object. |
Important: _.merge() mutates the original destination object. If you want to keep the original intact, pass an empty object as the first argument:
const result = _.merge({}, object1, object2);
The most common use case for _.merge() is combining two objects. Properties from the source object are copied into the destination. If both objects share a key, the source value overwrites the destination value.
const object1 = { a: 1, b: 2 };
const object2 = { b: 3, c: 4 };
const result = _.merge(object1, object2);
console.log(result);
// Output: { a: 1, b: 3, c: 4 }
Here, b existed in both objects, so the value from object2 (which is 3) takes priority.
You can also merge more than two objects at once:
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { c: 3, a: 99 };
const result = _.merge(obj1, obj2, obj3);
console.log(result);
// Output: { a: 99, b: 2, c: 3 }
The rightmost object always wins when keys conflict.
This is where _.merge() truly stands out. When objects contain nested properties, _.merge() recursively walks into each level and merges them — instead of replacing the entire nested object.
const object1 = {
user: {
name: "Alice",
address: { city: "Kolkata", zip: "700001" }
}
};
const object2 = {
user: {
address: { city: "Mumbai" },
role: "admin"
}
};
const result = _.merge({}, object1, object2);
console.log(result);
// Output:
// {
// user: {
// name: "Alice",
// address: { city: "Mumbai", zip: "700001" },
// role: "admin"
// }
// }
Notice what happened:
user.name ("Alice") was preserved because object2 didn't have it.user.address.city was updated to "Mumbai".user.address.zip ("700001") was preserved — this is the deep merge behavior.user.role ("admin") was added from object2.With Object.assign() or the spread operator, the entire user.address object would have been replaced, losing the zip property.
This is a commonly misunderstood behavior. Unlike what many articles incorrectly state, _.merge() does NOT concatenate arrays. Instead, it merges arrays by index — meaning element at index 0 of the source replaces element at index 0 of the destination, and so on.
const arr1 = [1, 2, 3];
const arr2 = [10, 20];
const result = _.merge([], arr1, arr2);
console.log(result);
// Output: [10, 20, 3]
Here is what happened:
1 was replaced by 102 was replaced by 203 was preserved (no corresponding index in arr2)When arrays contain objects, _.merge() deep merges the objects at matching indices:
const users1 = [
{ id: 1, name: "Alice", role: "developer" },
{ id: 2, name: "Bob" }
];
const users2 = [
{ id: 1, name: "Alice", role: "lead" },
{ id: 2, name: "Bob", role: "designer" }
];
const result = _.merge([], users1, users2);
console.log(result);
// Output:
// [
// { id: 1, name: "Alice", role: "lead" },
// { id: 2, name: "Bob", role: "designer" }
// ]
The objects at each index were deep merged — not replaced or concatenated.
If you need to concatenate arrays instead of merging by index, use _.mergeWith() with a customizer function:
const object1 = { tags: ["javascript", "lodash"] };
const object2 = { tags: ["node", "npm"] };
const result = _.mergeWith({}, object1, object2, (objValue, srcValue) => {
if (_.isArray(objValue)) {
return objValue.concat(srcValue);
}
});
console.log(result);
// Output: { tags: ["javascript", "lodash", "node", "npm"] }
_.mergeWith() accepts a customizer function as the last argument. This function is called for each property being merged, giving you full control over the merge logic.
Syntax:
_.mergeWith(object, sources, customizer)
The customizer function receives these arguments:
customizer(objValue, srcValue, key, object, source)
| Argument | Description |
|---|---|
objValue |
Current value in the destination object |
srcValue |
Value from the source object |
key |
The property key being merged |
object |
The destination object |
source |
The source object |
When the customizer returns undefined, the default merge behavior is used.
const scores1 = { math: 85, science: 92 };
const scores2 = { math: 90, science: 88 };
const result = _.mergeWith({}, scores1, scores2, (objValue, srcValue) => {
if (typeof objValue === 'number' && typeof srcValue === 'number') {
return Math.max(objValue, srcValue);
}
});
console.log(result);
// Output: { math: 90, science: 92 }
const config1 = { plugins: ["auth", "logger"] };
const config2 = { plugins: ["logger", "cache"] };
const result = _.mergeWith({}, config1, config2, (objValue, srcValue) => {
if (_.isArray(objValue)) {
return _.union(objValue, srcValue);
}
});
console.log(result);
// Output: { plugins: ["auth", "logger", "cache"] }
This is one of the most commonly asked comparisons. Here is exactly how they differ:
| Feature | _.merge() |
Object.assign() |
Spread {...} |
|---|---|---|---|
| Deep merge | ✅ Yes — recursively merges nested objects | ❌ No — replaces nested objects entirely | ❌ No — replaces nested objects entirely |
| Array handling | Merges arrays by index | Replaces arrays entirely | Replaces arrays entirely |
| Mutates destination | ✅ Yes | ✅ Yes | ❌ No — creates a new object |
Handles undefined |
Skips undefined values |
Copies undefined values |
Copies undefined values |
| Prototype properties | Copies inherited properties | Only own enumerable properties | Only own enumerable properties |
| Bundle size | ~6KB (lodash.merge) | 0KB (native) | 0KB (native) |
| Browser support | All (via npm) | ES6+ | ES2018+ |
const target = { a: 1, nested: { b: 2, c: 3 } };
const source = { nested: { c: 99 } };
// Using _.merge()
_.merge({}, target, source);
// Result: { a: 1, nested: { b: 2, c: 99 } } ← b is preserved ✅
// Using Object.assign()
Object.assign({}, target, source);
// Result: { a: 1, nested: { c: 99 } } ← b is LOST ❌
// Using Spread
{ ...target, ...source };
// Result: { a: 1, nested: { c: 99 } } ← b is LOST ❌
When to use which:
_.merge() when you need to deep merge nested objects or arraysObject.assign() for flat, one-level object merging{...} when you want a quick shallow copy/merge without mutationOne of the most common real-world uses — combining default settings with user overrides:
const defaultConfig = {
server: { port: 3000, host: "localhost" },
database: { host: "localhost", port: 5432, name: "myapp" },
logging: { level: "info", format: "json" }
};
const envConfig = {
server: { port: 8080 },
database: { host: "prod-db.example.com", name: "myapp_prod" },
logging: { level: "warn" }
};
const config = _.merge({}, defaultConfig, envConfig);
console.log(config);
// Output:
// {
// server: { port: 8080, host: "localhost" },
// database: { host: "prod-db.example.com", port: 5432, name: "myapp_prod" },
// logging: { level: "warn", format: "json" }
// }
All default values are preserved unless explicitly overridden.
When a user updates their profile, you typically want to modify specific fields without losing existing data:
const userProfile = {
username: "john_doe",
bio: "Web developer",
social: {
twitter: "johndoe_twitter",
linkedin: "johndoe_linkedin"
},
preferences: {
theme: "dark",
notifications: { email: true, push: false }
}
};
const updates = {
bio: "Full-stack developer",
social: { github: "johndoe_github" },
preferences: { notifications: { push: true } }
};
const updatedProfile = _.merge({}, userProfile, updates);
console.log(updatedProfile);
// Output:
// {
// username: "john_doe",
// bio: "Full-stack developer",
// social: {
// twitter: "johndoe_twitter",
// linkedin: "johndoe_linkedin",
// github: "johndoe_github"
// },
// preferences: {
// theme: "dark",
// notifications: { email: true, push: true }
// }
// }
Every existing field is preserved. Only the specified updates are applied.
When fetching paginated or partial data from APIs:
const cachedData = {
users: [
{ id: 1, name: "Alice", lastSeen: "2025-01-01" }
],
meta: { page: 1, total: 100 }
};
const freshData = {
users: [
{ id: 1, name: "Alice", lastSeen: "2025-02-09", online: true }
],
meta: { page: 1, total: 102 }
};
const merged = _.merge({}, cachedData, freshData);
// users[0] now has all fields: id, name, lastSeen (updated), and online (new)
> Next steps after merging: Once your data is merged, you can sort it with _.sortBy() or group it by category with _.groupBy() for display.
If you are using TypeScript, _.merge() works with proper type inference:
import merge from 'lodash/merge';
interface Config {
server: { port: number; host: string };
debug: boolean;
}
const defaults: Config = {
server: { port: 3000, host: "localhost" },
debug: false
};
const overrides: Partial<Config> = {
server: { port: 8080, host: "localhost" }
};
// TypeScript infers the return type correctly
const config: Config = merge({}, defaults, overrides);
Install type definitions if you are using the modular package:
npm install @types/lodash
const original = { a: 1, b: { c: 2 } };
const source = { b: { d: 3 } };
_.merge(original, source);
console.log(original);
// Output: { a: 1, b: { c: 2, d: 3 } } ← original is modified!
Fix: Always pass an empty object as the first argument if you want to keep originals intact:
const result = _.merge({}, original, source);
const obj1 = { a: 1, b: 2 };
const obj2 = { a: undefined, b: 3 };
_.merge({}, obj1, obj2);
// Output: { a: 1, b: 3 } ← a stays 1, undefined is skipped
This is different from Object.assign(), which would set a to undefined.
_.merge() does handle circular references internally using a stack-based approach, but deeply circular structures can lead to unexpected results. Avoid passing objects with circular references when possible.
This is a security concern. Older versions of Lodash (before 4.17.12) were vulnerable to prototype pollution via _.merge(). An attacker could inject properties into Object.prototype through crafted input:
// ⚠️ VULNERABLE in old versions
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
_.merge({}, malicious);
// In vulnerable versions, this could affect ALL objects:
// {}.isAdmin === true
How to protect yourself:
_.merge()Object.create(null) for sensitive merge targetsCheck your installed version:
npm list lodash
_.merge() is fast enough for most applications, but deep merging has a cost:
Tips for better performance:
Object.assign() or spread for flat objectslodash/merge instead of the full Lodash library to reduce bundle sizeModern JavaScript now offers native options that may reduce your dependency on Lodash:
// Deep clone first, then shallow merge
const result = { ...structuredClone(target), ...source };
This handles deep cloning but not deep merging — nested objects still get replaced.
function deepMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (
source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) &&
target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])
) {
result[key] = deepMerge(target[key], source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
This works for simple cases but lacks the edge case handling that _.merge() provides (inherited properties, typed arrays, buffers, etc.).
Use _.merge() when you need:
_.mergeWith()Use native alternatives when:
Object.assign())If you are working with Lodash for data manipulation, these guides cover the other essential methods:
_.orderBy(), and case-insensitive sorting._.sumBy() for aggregation._.merge() performs a deep recursive merge — it walks into nested objects and merges their individual properties. _.assign() (and Object.assign()) performs a shallow copy — nested objects are replaced entirely, not merged. Use _.merge() when you have nested data structures that need to be combined without losing inner properties.
No. _.merge() merges arrays by index, not by concatenation. Element at index 0 of the source overwrites element at index 0 of the destination. If you need to concatenate arrays during a merge, use _.mergeWith() with a customizer function that calls concat() on array values.
Yes. _.merge() modifies the destination object (the first argument) directly and returns it. To avoid mutation, pass an empty object {} as the first argument: _.merge({}, obj1, obj2).
Yes. _.merge() works in both browser and Node.js environments. Install via npm install lodash and import with const merge = require('lodash/merge') or import merge from 'lodash/merge'.
Lodash versions 4.17.12 and later include fixes for known prototype pollution vulnerabilities in _.merge(). Always use the latest version (4.17.21+) and sanitize any user-provided input before merging.
For shallow merging, use the native spread operator ({...obj1, ...obj2}) or Object.assign(). For deep merging without Lodash, you can write a custom recursive merge function or use libraries like deepmerge. However, _.merge() remains the most battle-tested option for complex deep merging scenarios.
Comments
Sign in to join the discussion.
No comments yet. Be the first to share your thoughts.