Combine Two or more JSON Objects in JavaScript
In this quick tutorial, we’ll learn how to combine two or more objects into one object in JavaScript, and what happens when the objects being merged have overlapping or nested keys.
A quick note on the title: these are plain JavaScript objects, not JSON (JSON is a text format — what you’d get from JSON.stringify()). The techniques below work the same either way since a JS object literal and a parsed JSON payload are both just objects at runtime, but it’s worth knowing the two aren’t literally the same thing.
Object.assign()
Object.assign(target, ...sources) copies the own enumerable properties of one or more source objects onto a target object, and returns that target.
var obj1 = { eat: 'pizza', drink: 'coke' };
var obj2 = { drive: 'car', ride: 'bus' }
var obj3 = { pet: 'dog' }
var obj4 = Object.assign({}, obj1, obj2, obj3);
console.log(obj4);
// { eat: 'pizza', drink: 'coke', drive: 'car', ride: 'bus', pet: 'dog' }
Watch the first argument. Passing an empty object {} as the target keeps obj1, obj2 and obj3 untouched. If you instead write Object.assign(obj1, obj2, obj3), obj1 itself is mutated and becomes the merged object — Object.assign always writes into its first argument and returns that same reference, it never creates a new object for you:
var target = { eat: 'pizza', drink: 'coke' };
var source = { drive: 'car' };
var result = Object.assign(target, source); // no {} — target is mutated!
console.log(result === target); // true — same object
console.log(target); // { eat: 'pizza', drink: 'coke', drive: 'car' }
Spread Operator (…)
If you’re on ES2018+ (Node.js and every modern browser), the object spread operator ... is the more idiomatic way to combine objects — it always produces a brand-new object, so there’s no risk of accidentally mutating one of the sources.
var obj1 = { eat: 'pizza', drink: 'coke' };
var obj2 = { drive: 'car', ride: 'bus' }
var obj3 = { pet: 'dog' }
var obj4 = { ...obj1, ...obj2, ...obj3 };
console.log(obj4);
// { eat: 'pizza', drink: 'coke', drive: 'car', ride: 'bus', pet: 'dog' }
Key Collisions: Last One Wins
Both approaches resolve collisions the same way: when two objects share a key, whichever object is applied last overwrites the earlier value. This is why order matters and is commonly used for setting defaults with overrides:
var defaults = { theme: 'light', fontSize: 12 };
var overrides = { fontSize: 16 };
console.log({ ...defaults, ...overrides }); // { theme: 'light', fontSize: 16 }
console.log({ ...overrides, ...defaults }); // { fontSize: 12, theme: 'light' }
Put your defaults first and user-supplied overrides last, and you get an override-friendly config object in one line.
Gotcha: Nested Objects Only Merge One Level Deep
Both Object.assign() and the spread operator perform a shallow merge — only top-level keys are combined. If a key holds an object in both sources, the later object’s value replaces the earlier one entirely rather than merging with it:
var user = { name: 'Alice', address: { city: 'Boston', zip: '02101' } };
var patch = { address: { zip: '10001' } };
var merged = { ...user, ...patch };
console.log(JSON.stringify(merged));
// {"name":"Alice","address":{"zip":"10001"}}
// city is gone — the whole address object was overwritten, not merged
There’s a second trap hiding here: because the merge is shallow, merged.address is not a copy — it’s the same reference as patch.address. Mutating one mutates the other:
console.log(merged.address === patch.address); // true
merged.address.city = 'NYC';
console.log(patch.address.city); // 'NYC' — patch.address changed too!
Deep Merging
If you need nested objects to merge recursively instead of being replaced, you have to walk the structure yourself (or use a library). A minimal recursive version:
function deepMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (
source[key] instanceof Object &&
!Array.isArray(source[key]) &&
target[key] instanceof Object
) {
result[key] = deepMerge(target[key], source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
console.log(JSON.stringify(deepMerge(user, patch)));
// {"name":"Alice","address":{"city":"Boston","zip":"10001"}}
For anything beyond a quick script, reach for a well-tested implementation instead of hand-rolling this — Lodash’s merge() handles arrays, null/undefined sources, and circular references correctly, edge cases that are easy to get wrong in a homemade version.
You may also see JSON.parse(JSON.stringify(obj)) used to deep-clone an object before merging it. It works for plain data, but silently drops functions, undefined values, and Date/Map/Set instances (dates become strings), so it’s not a safe general-purpose substitute for a real deep merge.