How do I build a nested object in Javascript dynamically?

Hi FCC,

I’ve been struggling with this problem for a day or two and I have been able to get close but not the full solution that I am looking for. Say I have two arrays like so,

const arr1 = ['a','b'];
const arr2 = ['a.foo', 'b.bar'];

The arrays will always be the same length. I want to iterate over the arrys and build a nested object that has this shape:

{
  aggs : {
    a: {
      terms: {
        field: 'a.foo'
      },
      aggs: {
        b : {
           terms: {
               field: 'b.bar'
           }
        }
     }
}

So I think what is making it difficult for me is that I need to nest each new item in the array inside the previously created object. I’ve tried a recursive solution and an iterative one and I just can’t get it. Can anyone help me out here?

THanks

@camperextraordinaire

EDIT: just realized this solution doesnt work, but its my closest attempt so far

const arr1 = ['a','b', 'c'];
const arr2 = ['a.foo', 'b.bar', 'c.baz'];

let result = {};

const insertAggs = (agg, result) => {

  while (result.aggs) {
    result = result.aggs
  }

  result.aggs = agg;
}

for (let i = 0; i < arr1.length; i++) {
  const aggName = arr1[i];
  const field = arr2[i];
  const agg = {
      [aggName]: {
        terms: {
          field
        }
      }
    };

  result.aggs ? insertAggs(agg, result) : result.aggs = agg;
}
}

I’ve had to do something similar before, so I think I can help. But I’m not following your data structures.

You have

const arr1 = ['a','b'];
const arr2 = ['a.foo', 'b.bar'];

And your end goal is this?

aggs : {
    a: {
      terms: {
        field: 'a.foo'
      },
      aggs: {
        b : {
           terms: {
               field: 'b.bar'
           }
        }
     }

So arr1 are the keys, and arr2 are the values? In which case you’re trying to assign

a.terms.field = 'a.foo'

Or are you trying to build the whole object like

a['terms']['field']['foo']

In order for me to help, I’ll need to know what values you explicitly know, and which will be dynamically assigned.

EDIT: The reason I ask is because it seems redundant to have two arrays, since you can determine the root node from arr2

Hi @JM-Mendez

I have given a bad example here with arr2. The values in arr2 are not related to arr1 at all. Below would be a more correct and less confusing explanation of my goal here

// arr2 values are unrelated to arr1
const arr1 = ['a','b'];
const arr2 = ['c', 'd'];

// end goal
{
 aggs: {
  a: { 
    terms:  {
      field : 'c',
    }, 
    aggs: {
      b: {
        terms: {
          field: 'd'
        }
      }
    }
}

Ok, so we know for a fact that the properties terms and field will exist for each root node in arr1. I know exactly how to do this. Give me a few minutes to write out and test solutions using reduce and for loops.

1 Like

Yes the shape is standard and each node will have a terms.field chunk.

A generic node looks like this

  aggs: {
    [valFromArr1] : {
       terms:  {
          field : [valueFromArr2]
       }
   }

The issue I’m having is nesting each node underneath the previously generated node.

Thanks so much for your help

Ok, you were close. The example you gave where you almost had it, you kept reassigning the whole object

const agg = {
      [aggName]: {
        terms: {
          field
        }
      }
    };

Which was throwing off your result. Instead, assign all 2nd level nodes on the root object. I also show below how to add a root level prop named aggs if you need it.

The thing to keep in mind is that in javascript you can only dynamically set properties for the next level in.

(I made it a spoiler in case this was for an exercise)

// arr.reduce
const withoutAggs = arr1.reduce((agg, nextKey, currIdx) => {
  agg[nextKey] = {
    terms: {
       fields: arr2[currIdx]
     }
  }
  
  return agg
}, {})

// for loop
const withoutAggs = {}

for(let i = 0; i < arr1.length; i++){
  withoutAggs[arr1[i]] = {
    terms: {
      fields: arr2[i]
    }
  }
}

// this gives you 
{
  a: {
    terms: {
      fields: 'c'
    }
  // ...
  }
}

// if you need aggs to be top level, just assign it after
const withAggsProp = {
  aggs: withoutAggs
}

// which gives you 
{
  aggs: {
    a: // ...
  }
}

If this doesn’t work for any reason or you have follow up questions, just hit me back.

I made my own version; using ES6’s object spread notation and some recursive shit.

const arr1 = ['a','b'];
const arr2 = ['a.foo', 'b.bar'];

function nested(a, b) {
  return (a.length || b.length)
    ? {
        aggs: {
          [a[0]]: {
            terms: {
              field: b[0]
            },
            ...nested(
              a.slice(1),
              b.slice(1)
            )
          }
        }
      }
    : {}
}

console.log(JSON.stringify(nested(arr1, arr2)))

Now, if it wasn’t recursive then I may be wrong lol and I misread.

Hi @JM-Mendez

Your code looks like it results in a flat data structure?

I.E. both will produce:

{a: { terms: { fields: 'blah'}},
 b: { terms: { fields: 'adsad'}}
}

where as I need B nested under A at the “terms” level.

i.e.

{ 
  a : { 
     terms: {
       fields: 'some val'
     },
     aggs: {
         b: {
            terms: {
              fields: 'something else'
            }
         }
     }
  }
}

Hope this makes sense and again thank you for the help

@luishendrix92

Nice this looks like a valid solution. Going to have to take some time for me to understand whats going on. Thanks a lot for your help! Really clever solution

Your code looks like it results in a flat data structure?

Yes it does. It looks like I misunderstood the requirements. So you needs each nested level to have a root of aggs? So this?

aggs.a
aggs.a.aggs.b
aggs.a.aggs.b.aggs.c

No problem - I probably didnt do a good job explaining.

This is the input/output:

const arr1 = ['a','b', 'c'];
const arr2 = ['d', 'e', 'f'];

var ans = yourFn(arr1, arr2);

console.log(ans.aggs.a.aggs.b.aggs.c.terms.field) //logs "f"

In this case it seems @luishendrix92 has the correct algorithm.

Yeah haha all I wanted to illustrate that the problem is recursive in nature.

I had a hard time building the code for it and I wanted less confusion by using the spread operator but if you want to use the correct tools for all browsers and engines:

for loop with bracket assignment but still recursive.

so instead {…} you just create an empty object and then assign stuff to it, and use another if statement to check if there’s a next property, case in which you’d assign a recursive call object to the terms sub-object. So you kinda need a nested if statement: the outer one to check if any of the lists have any elements left and the inner one to check if there’s a next one in BOTH arrays.

1 Like

So I was giving this some thought and realized that if the nested object is deep enough, or your nested objects were sufficiently heavy, then it could blow the stack using recursion.

So I thought to present you an iterative solution that works for any depth and object size without blowing the stack. This solution still runs in O(n) time like the recursive one, but improves space complexity from O(s + n) to O(n) – assuming n to be the size of the new object, and s to be the size of the recursive stack.

const arr1 = ['a', 'b', 'c']
const arr2 =  [1, 2, 3]

const obj = {}
let prevNode

for(let i = 0; i < arr1.length; i++){
  const key = arr1[i]
  const nextNode = {
      [key]: {
        terms: {
          fields: arr2[i]
        }
      }
    }

  if(!prevNode) {
    obj.aggs = nextNode
    prevNode = obj.aggs[key]
  } else {
    prevNode.aggs = nextNode
    prevNode = prevNode.aggs[key]
  }
}
2 Likes

Hey JM,

Really nice and simple iterative solution. I was trying to do something like this originally but just could not get it to work for whatever reason,

Thanks!

1 Like