Reputation: 20647
I am trying to create a function that takes a target json object a source json object (the update)
The rule is that the target must be updated by the source.
If the key are the same, the value of the target is overwritten.
If the key contains a dot .
we consider it as a nested update (example 3)
If the update key does not exist in the target, we create it.
var target = {
foo: 1,
}
var source = {
foo: 22,
}
var output = {
foo: 22
}
var target = {
foo: {
bar: 1
},
}
var source = {
foo: {
baz: 999
},
}
var output = {
foo: {
baz: 999
}
}
var target = {
foo: {
bar: 1,
baz: 99,
},
}
var source = {
'foo.bar': 22,
}
var output = {
foo: {
bar: 22,
baz: 99
}
}
We assume that numbers are invalid key for the source. For example, the below source will never occur
// this will never happen
let source = {
1: 'something'
}
However, update can update the value of an array at a specif index using the dot .
method
const source = {
foo: [0, 1,2,3, {four: 4}]
}
const target = {
'foo.0': 'zero',
'foo.4.four': 'four',
'foo.4.five': 5,
}
const output = {
foo: ['zero', 1, 2, 3, {four: 'four', five: 5}]
}
Hence array can not change in size using the dot update, to change the size, we need to overwrite the entire array
My function below works perfectly fine with the Example 1 but I cannot get it to work with example 2 and 3.
Instead I obtain:
var example2 = {
bar: 1,
baz: 999,
}
var example3 = {
foo: {
bar: 1,
baz: 99,
},
'foo.bar': 22,
}
function deepMerge(target, source) {
Object.entries(source).forEach(([key, value]) => {
if (value && typeof value === 'object') {
deepMerge(target[key] = target[key] || {}, value);
return;
}
target[key] = value;
});
return target;
}
Upvotes: 1
Views: 347
Reputation: 165069
I would perform the following actions
source
entries (key / value pairs)key
into an array, separate by .
. For non path-like keys, this will just be the key itself// utility function
const isObjectOrArray = obj => typeof obj === 'object' && obj !== null
const deepMerge = (target, source) => {
// create a shallow copy because who wants to mutate source data
const result = { ...target }
// Iterate source entries
Object.entries(source).forEach(([ path, val ]) => {
const segments = path.split('.')
// save the last segment to assign the value
const last = segments.pop()
// Find or create the deepest object by path
const deepest = segments.reduce((obj, segment) => {
// create objects if they aren't already
if (!isObjectOrArray(obj[segment])) {
// perhaps check if segment is a number and create an array instead
// ¯\_(ツ)_/¯ I'm tired
obj[segment] = {}
}
return obj[segment]
}, result)
// write the new value
deepest[last] = val
})
return result
}
var target = {
abc: 'abc',
bar: {
baz: 1
},
foo: {
bar: 1,
baz: 99,
},
nums: [0, 1,2,3, {four: 4}]
}
var source = {
'foo.bar': 22,
abc: 'def',
'nums.0': 'zero',
'nums.4.four': 'four',
'nums.4.five': 5,
bar: {
bar: 'bar!'
},
'brand.new.object': 'value'
}
const output = deepMerge(target, source)
console.info(output)
.as-console-wrapper {max-height: none !important; top: 0;}
Upvotes: 1
Reputation: 710
This should works for every example you provided:
function deepMerge(target: any, source: any) {
Object.keys(source).forEach((key: string) => {
if (key.includes('.')) {
const nested: any = {}
nested[key.split('.')[1]] = source[key]
target[key.split('.')[0]] = deepMerge(target[key.split('.')[0]], nested)
delete source[key]
}
});
return { ...target, ...source }
}
Upvotes: 1
Reputation: 509
Here if you break {'foo.bar': 22}
into { foo: {bar: 22}}
before giving it to input as a source you should get expected result.
function breakItApart(obj) {
Object.entries(obj).forEach(([key, value]) => {
[firstKey, anotherKey] = key.split('.')
if (anotherKey) {
obj[firstKey] = obj[firstKey] || {};
obj[firstKey][anotherKey] = value;
delete obj[key];
}
});
return obj;
}
This depends on the constraint that you would only get one dot in a key but you should be able extend this method for multiple dots.
var target = {
foo: {
bar: 1,
baz: 99,
},
}
var source = {
'foo.bar': 22,
}
const result = deepMerge(target, breakItApart(source));
console.log(result);
>> { foo: { bar: 22, baz: 99 } }
Upvotes: 0