Reputation: 21
This is a continuation of solution described in Combine json arrays by key, javascript, except that the input JSON has deeper level of nested array objects:
json1
[
{
"id": 1,
"name": "aaa",
"addresses": [
{
"type": "office",
"city": "office city"
},
{
"type": "home1",
"city": "home1 city"
}
]
},
{
"id": 2,
"name": "bbb",
"addresses": [
{
"type": "office",
"city": "office city"
},
{
"type": "home1",
"city": "home1 city"
}
]
}
]
json2
[
{
"id": 1,
"name": "aaa1",
"addresses": [
{
"type": "home1",
"city": "home1 new city"
},
{
"type": "home2",
"city": "home2 city"
}
]
},
{
"id": 3,
"name": "ccc",
"addresses": [
{
"type": "home1",
"city": "home1 city"
},
{
"type": "home2",
"city": "home2 city"
}
]
}
]
Expected result array
[
{
"id": 1,
"name": "aaa1",
"addresses": [
{
"type": "office",
"city": "office city"
},
{
"type": "home1",
"city": "home1 new city"
},
{
"type": "home2",
"city": "home2 city"
}
]
},
{
"id": 2,
"name": "bbb",
"addresses": [
{
"type": "office",
"city": "office city"
},
{
"type": "home1",
"city": "home1 city"
}
]
},
{
"id": 3,
"name": "ccc",
"addresses": [
{
"type": "home1",
"city": "home1 city"
},
{
"type": "home2",
"city": "home2 city"
}
]
}
]
When I followed the solution that georg suggested:
resultarray = _(json1).concat(json2).groupBy('id').map(_.spread(_.assign)).value();
the "addresses" attribute is getting overridden, instead of being merged. How to merge 2 JSON objects using a unique key and deep merge the nested array objects using another child unique key("type")?
Non lodash solutions are also welcome!
Upvotes: 2
Views: 1025
Reputation: 50787
Here's one approach. It assumes that you just want to merge the addresses and that other properties are kept intact. A general-purpose tool that merges all arrays would be somewhat more involved, and would not make it as easy to choose the correct address by type as done here. If you want such a tool, I haven't used it, but I saw recently in another answer one called deepmerge
.
Here's my implementation:
const last = xs =>
xs [xs.length - 1]
const group = (fn) => (xs) =>
Object .values (xs .reduce (
(a, x) => ((a [fn (x)] = [... (a [fn (x)] || []), x]), a),
{}
))
const mergeAddresses = (as) =>
as.reduce (({addresses: a1, ...rest1}, {addresses: a2, ...rest2}) => ({
...rest1,
...rest2,
addresses: group (a => a.type) ([...a1, ...a2]) .flatMap (last)
}))
const combine = (xs, ys) =>
group (x => x.id) ([...xs, ...ys]) .map (mergeAddresses)
const xs = [{id: 1, name: "aaa", addresses: [{type: "office", city: "office city"}, {type: "home1", city: "home1 city"}]}, {id: 2, name: "bbb", addresses: [{type: "office", city: "office city"}, {type: "home1", city: "home1 city"}]}]
const ys = [{id: 1, name: "aaa1", addresses: [{type: "home1", city: "home1 new city"}, {type: "home2", city: "home2 city"}]}, {id: 3, name: "ccc", addresses: [{type: "home1", city: "home1 city"}, {type: "home2", city: "home2 city"}]}]
console .log (combine (xs, ys))
.as-console-wrapper {max-height: 100% !important; top: 0}
We start with the trivial helper function last
, which simply takes the last element of an array.
Next is the more substantive helper group
. This is modeled after the API from Ramda's groupBy
(disclaimer: I'm a Ramda author), which is fairly similar to the one from lodash But groupBy
makes an indexed object out of an array. This then collects the values from that to turn it into an array of arrays. So while
groupBy(x => x.a)([{a: 1, b: 2}, {a:2, b: 5}, {a: 1, b: 7}, {a: 3, b: 8}, {a: 1, b: 9}])
yields
{
1: [{a: 1, b: 2}, {a: 1, b: 7}, {a: 1, b: 9}],
2: [{a: 2, b: 5}],
3: [{a: 3, b: 8}]
}
this group
function is slightly simpler:
group(x => x.a)([{a: 1, b: 2}, {a:2, b: 5}, {a: 1, b: 7}, {a: 3, b: 8}, {a: 1, b: 9}])
yields
[
[{a: 1, b: 2}, {a: 1, b: 7}, {a: 1, b: 9}],
[{a: 2, b: 5}],
[{a: 3, b: 8}]
]
The group
function might be worth keeping in a utility library. It has plenty of uses.
Next is a helper function, mergeAddresses
, which takes a list of objects and combines their addresses
arrays into a single array. The only complexity here is in handling the duplicated types. If we didn't have that concern, we could just write
addresses: [...a1, ...a2]
But here we do this:
addresses: group (a => a.type) ([...a1, ...a2]) .flatMap (last)
which groups the addresses by type and then takes the last one from each group.
Note that any properties other than addresses
from id-matching entries are simply passed along, with same-named properties from the second one overwriting those from the first one. (You could always change this by swapping rest1
and rest2
in the output object.)
Finally our main function, combine
, combines the arrays, uses group
to group them by id and then calls mergeAddresses
on each one.
After discussions of ways to make this more generic, there is a request for one particular abstraction, making "id"
, "addresses"
, and "type"
configurable. This is a fairly simple process, mechanical except for the choosing of parameter names. Here's an updated version:
const last = xs =>
xs [xs.length - 1]
const group = (fn) => (xs) =>
Object .values (xs .reduce (
(a, x) => ((a [fn (x)] = [... (a [fn (x)] || []), x]), a),
{}
))
const mergeChildren = (field, key) => (as) =>
as.reduce (({[field]: a1, ...rest1}, {[field]: a2, ...rest2}) => ({
...rest1,
...rest2,
[field]: group (a => a[key]) ([...a1, ...a2]) .flatMap (last)
}))
const combine = (parentKey, childField, childKey) => (xs, ys) =>
group (x => x [parentKey]) ([...xs, ...ys]) .map (mergeChildren (childField, childKey))
const combineByAddressType = combine ('id', 'addresses', 'type')
const xs = [{id: 1, name: "aaa", addresses: [{type: "office", city: "office city"}, {type: "home1", city: "home1 city"}]}, {id: 2, name: "bbb", addresses: [{type: "office", city: "office city"}, {type: "home1", city: "home1 city"}]}]
const ys = [{id: 1, name: "aaa1", addresses: [{type: "home1", city: "home1 new city"}, {type: "home2", city: "home2 city"}]}, {id: 3, name: "ccc", addresses: [{type: "home1", city: "home1 city"}, {type: "home2", city: "home2 city"}]}]
console .log (combineByAddressType (xs, ys))
.as-console-wrapper {max-height: 100% !important; top: 0}
We rename the helper function to mergeChild
, since this no longer has anything to do directly with addresses.
Depending upon how we want to use this, we can skip the partially applied helper function, combineByAddressType
, and directly call combine ('id', 'addresses', 'type') (xs, ys)
Upvotes: 0
Reputation: 2181
Here is a generic answer using object-lib.
// const objectLib = require('object-lib');
const { Merge } = objectLib;
const json1 = [{ id: 1, name: 'aaa', addresses: [{ type: 'office', city: 'office city' }, { type: 'home1', city: 'home1 city' }] }, { id: 2, name: 'bbb', addresses: [{ type: 'office', city: 'office city' }, { type: 'home1', city: 'home1 city' }] }];
const json2 = [{ id: 1, name: 'aaa1', addresses: [{ type: 'home1', city: 'home1 new city' }, { type: 'home2', city: 'home2 city' }] }, { id: 3, name: 'ccc', addresses: [{ type: 'home1', city: 'home1 city' }, { type: 'home2', city: 'home2 city' }] }];
const merge1 = Merge({
'[*]': 'id',
'[*].addresses[*]': 'type'
})
console.log(merge1(json1, json2));
// => [ { id: 1, name: 'aaa1', addresses: [ { type: 'office', city: 'office city' }, { type: 'home1', city: 'home1 new city' }, { type: 'home2', city: 'home2 city' } ] }, { id: 2, name: 'bbb', addresses: [ { type: 'office', city: 'office city' }, { type: 'home1', city: 'home1 city' } ] }, { id: 3, name: 'ccc', addresses: [ { type: 'home1', city: 'home1 city' }, { type: 'home2', city: 'home2 city' } ] } ]
// -----------
const d1 = { id: 1, other: [{ type: 'a', meta: 'X', prop1: true }] };
const d2 = { id: 1, other: [{ type: 'a', meta: 'Y', prop2: false }] };
console.log(Merge()(d1, d2))
// => { id: 1, other: [ { type: 'a', meta: 'X', prop1: true }, { type: 'a', meta: 'Y', prop2: false } ] }
console.log(Merge({ '**[*]': 'type' })(d1, d2))
// => { id: 1, other: [ { type: 'a', meta: 'Y', prop1: true, prop2: false } ] }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/[email protected]"></script>
Disclaimer: I'm the author of object-lib
Feel free to take a look at the source-code to get an idea how this works internally.
Upvotes: 1