18

I'm looking for something kind of like Object.keys but that works for potentially nested objects. It also shouldn't include keys that have object/array values (it should only include keys with immediate string/number/boolean values).

Example A

Input

{
   "check_id":12345,
   "check_name":"Name of HTTP check",
   "check_type":"HTTP"
}

Expected output

[
  "check_id",
  "check_name",
  "check_type"
]

Object.keys would work for flat cases like this, but not for nested cases:

Example B

Input

{
   "check_id":12345,
   "check_name":"Name of HTTP check",
   "check_type":"HTTP",
   "tags":[
     "example_tag"
   ],
   "check_params":{
      "basic_auth":false,
      "params":[
        "size"
      ],
      "encryption": {
        "enabled": true,
      }
   }
}

Expected output

[
  "check_id",
  "check_name",
  "check_type",
  "check_params.basic_auth",
  "check_params.encryption.enabled"
]

Note that this does not include tags, check_params, check_params.params, or check_params.encryption since these values are arrays/objects.

The question

Is there a library that does this? How would you implement it so that it can work with any object, large and nested, or small?

  • 1
    please add what you have tried and what does not work. – Nina Scholz Nov 01 '17 at 20:14
  • *It also shouldn't include keys that have object/array values* - but in your example B, `check_params` gets added to the list, even tho it's value is an object. – tymeJV Nov 01 '17 at 20:17
  • @tymeJV The `Expected output` only contains `check_params.basic_auth` and `check_params.encryption.enabled` (the nested keys), not the `check_params` itself that has the object value. –  Nov 01 '17 at 20:21

7 Answers7

38

You could use reduce like this:

const keyify = (obj, prefix = '') => 
  Object.keys(obj).reduce((res, el) => {
    if( Array.isArray(obj[el]) ) {
      return res;
    } else if( typeof obj[el] === 'object' && obj[el] !== null ) {
      return [...res, ...keyify(obj[el], prefix + el + '.')];
    }
    return [...res, prefix + el];
  }, []);

const input = {
   "check_id":12345,
   "check_name":"Name of HTTP check",
   "check_type":"HTTP",
   "tags":[
     "example_tag"
   ],
   "check_params":{
      "basic_auth":false,
      "params":[
        "size"
      ],
      "encryption": {
        "enabled": true,
        "testNull": null,
      }
   }
};

const output = keyify(input);

console.log(output);

Edit1: For the general case where you want to include arrays.

const keyify = (obj, prefix = '') => 
  Object.keys(obj).reduce((res, el) => {
    if( typeof obj[el] === 'object' && obj[el] !== null ) {
      return [...res, ...keyify(obj[el], prefix + el + '.')];
    }
    return [...res, prefix + el];
  }, []);

const input = {
   "check_id":12345,
   "check_name":"Name of HTTP check",
   "check_type":"HTTP",
   "tags":[
     "example_tag"
   ],
   "nested": [
      { "foo": 0 },
      { "bar": 1 }
   ],
   "check_params":{
      "basic_auth":false,
      "params":[
        "size"
      ],
      "encryption": {
        "enabled": true,
        "testNull": null,
      }
   }
};

const output = keyify(input);

console.log(output);
Olian04
  • 6,480
  • 2
  • 27
  • 54
  • @NinaScholz you mean if the `input` object is `null`? – Olian04 Nov 01 '17 at 21:34
  • maybe you change the value of `enabled` from `true` to `null`, then it does not work, because you are missing a check. – Nina Scholz Nov 01 '17 at 21:36
  • I have a similar question when we want to return all keys, please add this object { a: 1, b: 2, c: ["a", "b", "c"], z: { a: "j", q: "q" } it will return ["a", "b", "z.a", "z.q"], how can i show all keys like c? – Yasin Farmani Nov 27 '20 at 16:59
  • 1
    @YasinFarmani look at the end of my answer. "Edit1: For the general case where you want to include arrays." – Olian04 Nov 27 '20 at 21:25
8

A generator makes quick work of this kind of problem -

function* deepKeys (t, pre = [])
{ if (Array.isArray(t))
    return
  else if (Object(t) === t)
    for (const [k, v] of Object.entries(t))
      yield* deepKeys(v, [...pre, k])
  else
    yield pre.join(".")
}

const input =
  {check_id:12345,check_name:"Name of HTTP check",check_type:"HTTP",tags:["example_tag"],check_params:{basic_auth:false,params:["size"],encryption:{enabled:true,testNull:null,}}}
 
console.log(Array.from(deepKeys(input)))
[ "check_id"
, "check_name"
, "check_type"
, "check_params.basic_auth"
, "check_params.encryption.enabled"
, "check_params.encryption.testNull"
]

Or a pure functional expression which eagerly computes all keys -

const deepKeys = (t, pre = []) =>
  Array.isArray(t)
    ? []
: Object(t) === t
   ? Object
      .entries(t)
      .flatMap(([k, v]) => deepKeys(v, [...pre, k]))
: pre.join(".")

const input =
  {check_id:12345,check_name:"Name of HTTP check",check_type:"HTTP",tags:["example_tag"],check_params:{basic_auth:false,params:["size"],encryption:{enabled:true,testNull:null,}}}
 
console.log(deepKeys(input))
[ "check_id"
, "check_name"
, "check_type"
, "check_params.basic_auth"
, "check_params.encryption.enabled"
, "check_params.encryption.testNull"
]
Mulan
  • 129,518
  • 31
  • 228
  • 259
2

You could check the keys and iterate otherwise push the path to the result set.

function getKeys(object) {
    function iter(o, p) {
        if (Array.isArray(o)) { return; }
        if (o && typeof o === 'object') {
            var keys = Object.keys(o);
            if (keys.length) {
                keys.forEach(function (k) { iter(o[k], p.concat(k)); });
            }
            return;
        }
        result.push(p.join('.'));
    }
    var result = [];
    iter(object, []);
    return result;
}

var object = { check_id: 12345, check_name: "Name of HTTP check", check_type: "HTTP", tags: ["example_tag"], check_params: { basic_auth: false, params: ["size"], encryption: { enabled: true } } };

console.log(getKeys(object));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
1

You can use for...in and create recursive function.

var obj = {"check_id":12345,"check_name":"Name of HTTP check","check_type":"HTTP","tags":["example_tag"],"check_params":{"basic_auth":false,"params":["size",{"a":"b"}],"encryption":{"enabled":true}}}

var keys = []
function getKeys(data, k = '') {
  for (var i in data) {
    var rest = k.length ? '.' + i : i

    if (typeof data[i] == 'object') {
      if (!Array.isArray(data[i])) {
        getKeys(data[i], k + rest)
      }
    } else keys.push(k + rest)
  }
}

getKeys(obj)
console.log(keys)
Nenad Vracar
  • 118,580
  • 15
  • 151
  • 176
  • 1
    Having such blatantly unnecessary side effects as a global `keys` variable might convey the wrong message to new JS programmers. Just a thought though. – Olian04 Nov 01 '17 at 20:43
0

var json = {
    id: '1234',
    test: 'terst',
    user : {
        name: '',
        details: {
            address: {
                    add2: {
                        prim: "",
                        sec: ""
                    },
                    add1: '',
            }
        }
    },
    salary: {
      cur: 1234,
      net: 89797
    },
    age: 12
}

let arr = [];
let initialObj = {};

function getKeys(obj, parentK=''){
  initialObj = arr.length === 0 ? obj: initialObj;
  const entries = Object.entries(obj);
  for(let i=0; i<entries.length; i++) {
    const key = entries[i][0];
    const val = entries[i][1];
    const isRootElement = initialObj.hasOwnProperty(key);
    parentK = isRootElement ? key: parentK+'.'+key;
    arr.push(parentK)
    if(typeof val === 'object' && val!==null && !Array.isArray(val)){
      getKeys(val, parentK);
    }
  }
}

getKeys(json)

console.log('arr final---', arr);
0

Further enhanced above recommendation to return all keys including array.

 const keyify = (obj, prefix = '') => 
  Object.keys(obj).reduce((res, el) => {
    if( Array.isArray(obj[el]) ) {
      return [...res,`${el}: ${obj[el].toString()}`];
    } else if( typeof obj[el] === 'object' && obj[el] !== null ) {
      return [...res,...keyify(obj[el],`${prefix}${el}.`)];
    }
    return [...res,`${prefix}${el}: ${obj[el]}`];
  }, []);
  
const input = {
   "check_id":12345,
   "check_name":"Name of HTTP check",
   "check_type":"HTTP",
   "tags":[
     "example_tag"
   ],
   "check_params":{
      "basic_auth":false,
      "params":[
        "size"
      ],
      "encryption": {
        "enabled": true,
        "testNull": null,
      }
   }
};

const output = keyify(input);
console.log(output);

Expected output:

[
  'check_id: 12345',
  'check_name: Name of HTTP check',
  'check_type: HTTP',
  'tags: example_tag',
  'check_params.basic_auth: false',
  'params: size',
  'check_params.encryption.enabled: true',
  'check_params.encryption.testNull: null'
]
crystal
  • 185
  • 1
  • 4
  • 1
    You might want to read _[How do I create a runnable stack snippet?](https://meta.stackoverflow.com/questions/358992)_. – Scott Sauyet Jan 23 '23 at 14:07
  • Also, note that this is an odd output format, and an odd way to extend the OP's requirements. Wouldn't this be more appropriate: `{'check_id': 12345, ..., 'check_params.encryption.testNull': null}` ? It wouldn't be much more difficult to generate. – Scott Sauyet Jan 23 '23 at 14:18
-1

Is this what you mean?

http://jsfiddle.net/robbiemilejczak/hfe12brb/1/

I couldn't do it with vanilla JS, and this is a pretty hacky solution that relies on lodash. Basically leverages lodashs _.forIn and _.isArray functions to iterate over an object. Also this will only go 1 layer deep, so objects inside of nested objects will be ignored. It does produce your expected output though, so I'd say it's a decent starting point.

Robbie Milejczak
  • 5,664
  • 3
  • 32
  • 65