Lodash _.merge() Deep Merge Objects & Arrays — Guide with Examples
Share:

Lodash _.merge() Deep Merge Objects & Arrays — Guide with Examples

Amaresh Adak

By Amaresh Adak

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.

How to Install and Import Lodash Merge

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.

Lodash Merge Syntax and Parameters

Here is the complete syntax for _.merge():

_.merge(object, [sources])
ParameterTypeDescription
objectObjectThe destination object. This object is mutated directly.
[sources]...ObjectOne or more source objects to merge into the destination.
ReturnsObjectReturns 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);

Merging Two Objects (Top-Level Merge)

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.

Deep Merge Nested Objects with Lodash

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.

Lodash Merge Arrays — How It Actually Works

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:

  • Index 0: 1 was replaced by 10
  • Index 1: 2 was replaced by 20
  • Index 2: 3 was preserved (no corresponding index in arr2)

Lodash Merge Array of Objects

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.

How to Actually Concatenate Arrays During Merge

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"] }

Customize Merge Behavior with _.mergeWith()

_.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)
ArgumentDescription
objValueCurrent value in the destination object
srcValueValue from the source object
keyThe property key being merged
objectThe destination object
sourceThe source object

When the customizer returns undefined, the default merge behavior is used.

Example: Keep the Larger Value

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 }

Example: Deduplicate Arrays During Merge

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"] }

Lodash Merge vs Object.assign vs Spread Operator

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 handlingMerges arrays by indexReplaces arrays entirelyReplaces arrays entirely
Mutates destination✅ Yes✅ Yes❌ No — creates a new object
Handles undefinedSkips undefined valuesCopies undefined valuesCopies undefined values
Prototype propertiesCopies inherited propertiesOnly own enumerable propertiesOnly own enumerable properties
Bundle size~6KB (lodash.merge)0KB (native)0KB (native)
Browser supportAll (via npm)ES6+ES2018+

Side-by-Side Code Comparison

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:

  • Use _.merge() when you need to deep merge nested objects or arrays
  • Use Object.assign() for flat, one-level object merging
  • Use spread {...} when you want a quick shallow copy/merge without mutation

Practical Examples of Lodash Merge

Merging Configuration Objects

One 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.

Updating User Profiles

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.

Merging API Responses

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.

Lodash Merge with TypeScript

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

Common Pitfalls and Edge Cases

1. Mutation — _.merge() Changes the Original Object

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);

2. undefined Values Are Skipped

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.

3. Circular References Can Cause Issues

_.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.

4. Prototype Pollution Vulnerability

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:

  • Always use Lodash 4.17.21 or later (latest stable)
  • Sanitize user input before passing it to _.merge()
  • Consider using Object.create(null) for sensitive merge targets

Check your installed version:

npm list lodash

Performance Considerations

_.merge() is fast enough for most applications, but deep merging has a cost:

  • Time complexity: O(n) where n is the total number of properties across all levels
  • Space complexity: O(d) where d is the maximum nesting depth (due to recursion stack)

Tips for better performance:

  • Only deep merge when necessary — use Object.assign() or spread for flat objects
  • Import lodash/merge instead of the full Lodash library to reduce bundle size
  • For very large or deeply nested objects, consider using Immer which uses structural sharing

Native Alternatives to Lodash Merge in 2025

Modern JavaScript now offers native options that may reduce your dependency on Lodash:

structuredClone() + Spread (ES2022+)

// Deep clone first, then shallow merge
const result = { ...structuredClone(target), ...source };

This handles deep cloning but not deep merging — nested objects still get replaced.

Custom Recursive Deep Merge

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.).

When to Still Use Lodash Merge

Use _.merge() when you need:

  • Deep merging of nested objects with reliable edge case handling
  • Array-by-index merging behavior
  • Custom merge logic via _.mergeWith()
  • Battle-tested, well-maintained code in production

Use native alternatives when:

  • You only need shallow merging (spread or Object.assign())
  • Bundle size is critical and you have simple merge needs
  • Your project already avoids external dependencies

If you are working with Lodash for data manipulation, these guides cover the other essential methods:

FAQs

What is the difference between Lodash merge and assign?

_.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.

Does Lodash merge concatenate arrays?

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.

Does Lodash merge mutate the original object?

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).

Can I use Lodash merge in Node.js?

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'.

Is Lodash merge safe from prototype pollution?

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.

What can I use instead of Lodash merge?

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.

Ready to Implement This in Production?

Skip the months of development and debugging. Our team will implement this solution with enterprise-grade quality, security, and performance.

Web Apps ₹25K+
Mobile Apps ₹75K+