noam suissa
noam suissa

Reputation: 468

Retrieve paths of deepest properties in an object

I am trying to find a way to dynamically create an array that contains the paths of the deepest properties in a nested object. For example, if my object is the following:

{
    userName: [],
    email: [],
    name: {
        fullName: [],
        split: {
            first: [],
            last: []
        }
    },
    date: {
        input: {
            split: {
                month: [],
                year: []
            },
            full: []
        },
        select: {
            month: [],
            year: []
        }
    }
};

I would need an array to contain something like:

["userName", "email", "name.fullName", "name.split.first",...]

Are there any built-in or external libraries that do this automatically? I was trying to use Object.keys on the parent object but this only returns the direct children properties.

Upvotes: 0

Views: 762

Answers (3)

Mulan
Mulan

Reputation: 135357

You can use Array.prototype.flatMap for this -

const d =
  {userName:[],email:[],name:{fullName:[],split:{first:[],last:[]}},date:{input:{split:{month:[],year:[]},full:[]},select:{month:[],year:[]}}}

const main = (o = {}, path = []) =>
  Array.isArray(o) || Object(o) !== o
    ? [ path ]
    : Object
        .entries(o)
        .flatMap(([ k, v ]) => main(v, [...path, k ]))
        
console.log(main(d))

Output

[ [ "userName" ]
, [ "email" ]
, [ "name", "fullName" ]
, [ "name" ,"split", "first" ]
, [ "name", "split", "last" ]
, ...
]

If you want the paths to be "a.b.c" instead of [ "a", "b", "c" ], use .map and Array.prototype.join -

const d =
  {userName:[],email:[],name:{fullName:[],split:{first:[],last:[]}},date:{input:{split:{month:[],year:[]},full:[]},select:{month:[],year:[]}}}

const main = (o = {}, path = []) =>
  Array.isArray(o) || Object(o) !== o
    ? [ path ]
    : Object
        .entries(o)
        .flatMap(([ k, v ]) => main(v, [...path, k ]))
        
console.log(main(d).map(path => path.join(".")))

Output

[
  "userName",
  "email",
  "name.fullName",
  "name.split.first",
  "name.split.last",
  "date.input.split.month",
  "date.input.split.year",
  "date.input.full",
  "date.select.month",
  "date.select.year"
]

If you do not want to rely on Array.prototype.flatMap because it is not supported in your environment, you can use a combination of Array.prototype.reduce and Array.prototype.concat -

const d =
  {userName:[],email:[],name:{fullName:[],split:{first:[],last:[]}},date:{input:{split:{month:[],year:[]},full:[]},select:{month:[],year:[]}}}

const main = (o = {}, path = []) =>
  Array.isArray(o) || Object(o) !== o
    ? [ path ]
    : Object
        .entries(o)
        .reduce // <-- manual flatMap
          ( (r, [ k, v ]) =>
              r.concat(main(v, [...path, k ]))
          , []
          )
        
console.log(main(d).map(path => path.join(".")))


Or you could polyfill Array.prototype.flatMap -

Array.prototype.flatMap = function (f, context)
{ return this.reduce
    ( (r, x, i, a) => r.concat(f.call(context, r, x, i, a))
    , []
    )
}

Is there a way to access any of those properties' value? Eg. "d.name.split.first" using the returned array at position 3?

We can write a lookup function that accepts an object, o, and a dot-separated string, s, that returns a value, if possible, otherwise returns undefined if s is unreachable -

const d =
  {userName:[],email:[],name:{fullName:[],split:{first:[],last:[]}},date:{input:{split:{month:[],year:[]},full:[]},select:{month:[],year:[]}}}
  
const lookup = (o = {}, s = "") =>
  s
    .split(".")
    .reduce
      ( (r, x) =>
          r == null ? undefined : r[x]
      , o
      )
 
console.log(lookup(d, "name.split"))
// { first: [], last: [] }

console.log(lookup(d, "name.split.first"))
// []

console.log(lookup(d, "name.split.zzz"))
// undefined

console.log(lookup(d, "zzz"))
// undefined

Upvotes: 2

Holli
Holli

Reputation: 5082

come on. Arrays and Objects are basically the same thing. There is absolutely no need for undefined checks.

data = { ... };

function paths_list( value, result=[], path=[] )
{
    for ( keydx in value )
    {
        if ( value[keydx] instanceof Object )
        {
            path.push( keydx );
            result.push( path.join(".") );
            paths_list( value[keydx], result, path );
            path.pop();
        }
    }

    return result;
}

console.log( paths_list(data) );

Prints

Array ["userName", "email", "name", "name.fullName", "name.split", "name.split.first", "name.split.last", "date", "date.input", "date.input.split", "date.input.split.month", "date.input.split.year", "date.input.full", "date.select", "date.select.month", "date.select.year"]

Upvotes: 1

epascarello
epascarello

Reputation: 207527

There are many ways to do it. Easiest is just a recursion to test if you have an Object and loop over the keys keeping track of the path on each step.

var myObj = {
    userName: [],
    email: [],
    name: {
        fullName: [],
        split: {
            first: [],
            last: []
        }
    },
    date: {
        input: {
            split: {
                month: [],
                year: []
            },
            full: []
        },
        select: {
            month: [],
            year: []
        }
    }
}

function setPath(a, b) {
  return a.length ? a + '.' + b : b
}

function getAllPaths(obj, paths, currentPath) {
  if (paths===undefined) paths = []
  if (currentPath===undefined) currentPath = ''
  Object.entries(obj).forEach( function (entry) {
    const updatedPath = setPath(currentPath, entry[0])
    if (entry[1] instanceof Object && !Array.isArray(entry[1])) { 
      getAllPaths(entry[1], paths, updatedPath)
    } else {
      paths.push(updatedPath)
    }
  })
  return paths
}


console.log(getAllPaths(myObj))

written with arrow functions and default values

var myObj = {
    userName: [],
    email: [],
    name: {
        fullName: [],
        split: {
            first: [],
            last: []
        }
    },
    date: {
        input: {
            split: {
                month: [],
                year: []
            },
            full: []
        },
        select: {
            month: [],
            year: []
        }
    }
}

const setPath = (a, b) => a.length ? a + '.' + b : b

const getAllPaths = (obj, paths=[], currentPath='') => {
  Object.entries(obj).forEach( ([key, value]) => {
    const updatedPath = setPath(currentPath, key)
    if (value instanceof Object && !Array.isArray(value)) { 
      getAllPaths(value, paths, updatedPath)
    } else {
      paths.push(updatedPath)
    }
  })
  return paths
}


console.log(getAllPaths(myObj))

Upvotes: 1

Related Questions