How to split object into nested object? (Recursive way)

You could use reduce method that will create similar nested structure with just objects.

var data = {
  m_name: 'my name',
  m_address: 'my address',
  p_1_category: 'cat 1',
  p_1_name: 'name 1',
  p_2_category: 'cat 2',
  p_2_name: 'name 2',
  p_2_contact: '1234567',
  k_id: 111,
  k_name: 'abc'
}


const result = Object
  .entries(data)
  .reduce((a, [k, v]) => {
    k.split('_').reduce((r, e, i, arr) => {
      return r[e] || (r[e] = arr[i + 1] ? {} : v)
    }, a)

    return a
  }, {})

console.log(result)


I don't know if that output format was what you were really looking for or simply the best you were able to accomplish. One alternative would be to generate something like this:

{
  m: {name: "my name", address: "my address"},
  p: [
    {category: "cat 1", name: "name 1"},
    {category: "cat 2", name: "name 2"}
  ]
}

There is one major difference between this and your code's output. p is now a plain array of objects rather than a 1- and 2-indexed object. It's quite possible that this is not helpful to you, but it's an interesting alternative. There is also a second difference from the sample output you supplied. Both your original code and the answer from Nenad return m: {name: "my name", address: "my address"} instead of the requested m: [{name: "my name"}, {address: "my address"}]. This seems much more logical to me, and I've also done it this way.

Here is some code that would do this:

// Utility functions

const isInt = Number.isInteger

const path = (ps = [], obj = {}) =>
  ps .reduce ((o, p) => (o || {}) [p], obj)

const assoc = (prop, val, obj) => 
  isInt (prop) && Array .isArray (obj)
    ? [... obj .slice (0, prop), val, ...obj .slice (prop + 1)]
    : {...obj, [prop]: val}

const assocPath = ([p = undefined, ...ps], val, obj) => 
  p == undefined
    ? obj
    : ps.length == 0
      ? assoc(p, val, obj)
      : assoc(p, assocPath(ps, val, obj[p] || (obj[p] = isInt(ps[0]) ? [] : {})), obj)

// Main function

const hydrate = (flat) => 
  Object .entries (flat) 
    .map (([k, v]) => [k .split ('_'), v])
    .map (([k, v]) => [k .map (p => isNaN (p) ? p : p - 1), v])
    .reduce ((a, [k, v]) => assocPath (k, v, a), {})

// Test data

const data = {m_name: 'my name', m_address: 'my address', p_1_category: 'cat 1', p_1_name: 'name 1', p_2_category: 'cat 2', p_2_name: 'name 2' }

// Demo

console .log (
  hydrate (data)
)
.as-console-wrapper {min-height: 100% !important; top: 0}

This code is inspired by Ramda (of which I'm an author). The utility functions path, assoc, and assocPath have similar APIs to Ramda's, but these are unique implementations (borrowed from another answer.) Since these are built into Ramda, only the hydrate function would be necessary if your project used Ramda.

The big difference between this and Nenad's (excellent!) answer is that our object hydration takes into account the difference between string keys, which are assumed to be for plain objects, and numeric ones, which are assumed to be for arrays. However, since these are split out of our initial string (p_1_category), this could lead to ambiguity if you might sometimes want those to be objects.

I also use a trick that is a bit ugly and maybe wouldn't scale to other numeric values: I subtract 1 from the number so that the 1 in p_1_category maps to the zeroth index. If your input data looked like p_0_category ... p_1_category instead of p_1_category ... p_2_category we could skip this.

In any case, there is a real chance that this is contrary to your underlying requirements, but it might be useful to others.


no sorting required

The proposed output in your post does not follow a pattern. Some items group to arrays while others group to objects. Since array-like objects behave like arrays, we'll just use objects.

The output in this answer is the same as Nenad's but this program does not require that the object's keys are sorted beforehand -

const nest = (keys = [], value) =>
  keys.reduceRight((r, k) => ({ [k]: r }), value)

const result =
  Object
    .entries(data)
    .map(([ k, v ]) => nest(k.split("_"),  v))
    .reduce(merge, {})

console.log(result)

Output -

{
  m: {
    name: "my name",
    address: "my address"
  },
  p: {
    1: {
      category: "cat 1",
      name: "name 1"
    },
    2: {
      category: "cat 2",
      name: "name 2",
      contact: "1234567"
    }
  },
  k: {
    id: 111,
    name: "abc"
  }
}

merge

I'm borrowing a generic merge that I wrote in another answer. The advantages to reusing generic functions are numerous and I won't reiterate them here. Read the original post if you'd like to know more -

const isObject = x =>
  Object (x) === x

const mut = (o = {}, [ k, v ]) =>
  (o[k] = v, o)

const merge = (left = {}, right = {}) =>
  Object.entries (right)
    .map
      ( ([ k, v ]) =>
          isObject(v) && isObject(left[k])
            ? [ k, merge (left[k], v) ]
            : [ k, v ]
      )
    .reduce(mut, left)

Shallow merges work as expected -

const x =
  [ 1, 2, 3, 4, 5 ]

const y =
  [  ,  ,  ,  ,  , 6 ]

const z =
  [ 0, 0, 0 ]

console.log(merge(x, y))
// [ 1, 2, 3, 4, 5, 6 ]

console.log(merge(y, z))
// [ 0, 0, 0, <2 empty items>, 6 ]

console.log(merge(x, z))
// [ 0, 0, 0, 4, 5, 6 ]

And deep merges too -

const x =
  { a: [ { b: 1 }, { c: 1 } ] }

const y =
  { a: [ { d: 2 }, { c: 2 }, { e: 2 } ] }

console.log(merge (x, y))
// { a: [ { b: 1, d: 2 }, { c: 2 }, { e: 2 } ] }

Expand the snippet below to see our result in your own browser -

const isObject = x =>
  Object(x) === x

const mut = (o = {}, [ k, v ]) =>
  (o[k] = v, o)

const merge = (left = {}, right = {}) =>
  Object
    .entries(right)
    .map
      ( ([ k, v ]) =>
          isObject(v) && isObject(left[k])
            ? [ k, merge(left[k], v) ]
            : [ k, v ]
      )
    .reduce(mut, left)

const nest = (keys = [], value) =>
  keys.reduceRight((r, k) => ({ [k]: r }), value)

const data =
  {m_name:'my name',m_address:'my address',p_1_category:'cat 1',p_1_name:'name 1',p_2_category:'cat 2',p_2_name:'name 2',p_2_contact:'1234567',k_id:111,k_name:'abc'}

const result =
  Object
    .entries(data)
    .map(([ k, v ]) => nest(k.split("_"),  v))
    .reduce(merge, {})
  
console.log(JSON.stringify(result, null, 2))