rpd
rpd

Reputation: 21

How to merge 2 jsons using a unique key and deep merge the nested array objects using another child unique key

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

Answers (2)

Scott Sauyet
Scott Sauyet

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.

Update

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

vincent
vincent

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

Related Questions