2

I have a deeply nested object that I need to search to remove certain keys. The keys to remove are stored in an array indicated in the removeKeys array. Currently, the function only filters the top level object but scales the rest fine, it just does not filter child objects. How would I reduce the full object properly to get the desired output?

The initial unfiltered object:

let item = {
            "label": "test",
            "id": "test",
            "styles": {
                "label": "Styles",
                "styles": {
                    "test": {
                        "test": "test",
                        "label": "test",
                        "test1": {
                            "label": "test",
                            "image": {
                                "label": "test",
                                "type": "test",
                                "value": "test",
                                "autoSelect": "",
                                "id": ""
                            }
                        }
                    }
                }
            },
            "test": {
                "label": "test",
                "test": []
            }
        }

The keys to remove from the object:

const removeKeys = ["label", "type", "autoSelect"];

The recursive function to filter the nested object:

let filterObject = filterNestObject(item);

function filterNestObject(item) {
  return Object.keys(item)
  .filter(key => {
    if (typeof item[key] === 'object') filterNestObject(item[key]);

    if (!removeKeys.includes(key)) return true;

    return false 
  })  
  .reduce((object, key) => {
    return {
      ...object,
      [key]: item[key]
    };
  }, {});

}

The expected result would be:

{
            "id": "test",
            "styles": {
                "styles": {
                    "test": {
                        "test": "test",
                        "test1": {
                            "image": {
                                "value": "test",
                                "id": ""
                            }
                        }
                    }
                }
            },
            "test": {
                "test": []
            }
        }
alt-rock
  • 347
  • 1
  • 6
  • 18

6 Answers6

2

The mistake in your code is that you do the recursive call in the filter callback. But there you lose the object returned from the recursive call. Instead make it in the reduce callback.

A minor correction: to test whether a value is an object it is not enough to do typeof item[key] === "object" as null would also pass that test. Here is the adapted code:

function filterNestObject(item) {
    return Object.keys(item)
        .filter(key => !removeKeys.includes(key))  
        .reduce((acc, key) => {
            return Object.assign(acc, {
              [key]: Object(item[key]) === item[key] ? filterNestObject(item[key]) : item[key]
            });
        }, Array.isArray(item) ? [] : {});
}

const item = {"label": "test","id": "test","styles": {"label": "Styles","styles": {"test": {"test": "test","label": "test","test1": {"label": "test","image": {"label": "test","type": "test","value": "test","autoSelect": "","id": ""}}}}},"test": {"label": "test","test": []}};
const removeKeys = ["label", "type", "autoSelect"];
const filterObject = filterNestObject(item);
console.log(filterObject);
trincot
  • 317,000
  • 35
  • 244
  • 286
1

It's a bit hacky and not very performant so it may not be a good solution if you're dealing with very large object graphs, but here's a one-liner solution using the replacer callback in JSON.stringify:

JSON.parse(JSON.stringify(audience, (k, v) => removeKeys.includes(k) ? undefined : v));

Demo:

let audience = {
  "label": "test",
  "id": "test",
  "styles": {
    "label": "Styles",
    "styles": {
      "test": {
        "test": "test",
        "label": "test",
        "test1": {
          "label": "test",
          "image": {
            "label": "test",
            "type": "test",
            "value": "test",
            "autoSelect": "",
            "id": ""
          }
        }
      }
    }
  },
  "test": {
    "label": "test",
    "test": []
  }
}
const removeKeys = ["label", "type", "autoSelect"];
let newAudience = JSON.parse(JSON.stringify(audience, (k, v) => removeKeys.includes(k) ? undefined : v));
console.log(newAudience);

Along the same lines, if you're parsing the original object from a JSON string, you can use the reviver callback from JSON.parse:

let jsonString = `{
  "label": "test",
  "id": "test",
  "styles": {
    "label": "Styles",
    "styles": {
      "test": {
        "test": "test",
        "label": "test",
        "test1": {
          "label": "test",
          "image": {
            "label": "test",
            "type": "test",
            "value": "test",
            "autoSelect": "",
            "id": ""
          }
        }
      }
    }
  },
  "test": {
    "label": "test",
    "test": []
  }
}`
const removeKeys = ["label", "type", "autoSelect"];
const audience = JSON.parse(jsonString, (k, v) => removeKeys.includes(k) ? undefined : v);
console.log(audience);
p.s.w.g
  • 146,324
  • 30
  • 291
  • 331
  • The downside here is that it only works with simple data types. As soon as the input data has values like `Set`, `Map`, `Date`, `RegExp`, ... etc, it will not work. – trincot Apr 25 '19 at 20:39
  • @trincot correct, this is only for values that are serializable to/from JSON. As I said, it's kind of a hack, and not the right solution for all scenarios. – p.s.w.g Apr 25 '19 at 20:47
  • Very interesting for the right scenario of objects with strict data types. – alt-rock Apr 25 '19 at 21:40
1

You could filter the keys and build new objects by taking an iterative and recursive approach.

function remove(object, keys) {
    return Object.assign({}, ...Object.keys(object)
        .filter(k => !keys.includes(k))
        .map(k => ({ [k]: object[k] && typeof object[k] === 'object' ? remove(object[k], keys) : object[k] }))
    );
}

var item = { label: "test", id: "test", styles: { label: "Styles", styles: { test: { test: "test", label: "test", test1: { label: "test", image: { label: "test", type: "test", value: "test", autoSelect: "", id: "" } } } } }, test: { label: "test", test: [] } },
    removeKeys = ["label", "type", "autoSelect"];

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

Some time ago, I made an attempt to create a cloneObj() method to deep-clone an object using the new proposal Object.fromEntries(). You can check, for reference, the question I made in that moment on next link: Deep-Cloning an object using Object.fromEntries()

I believe this method can be slightly modified to suit your goals:

const item = {"label": "test","id": "test","styles": {"label": "Styles","styles": {"test": {"test": "test","label": "test","test1": {"label": "test","image": {"label": "test","type": "test","value": "test","autoSelect": "","id": ""}}}}},"test": {"label": "test","test": [{label: "foo", test: "test4"}]}};
const removeKeys = ["label", "type", "autoSelect"];

const cloneObjWithoutKeys = (obj, keys) =>
{
    if (Object(obj) !== obj)
       return obj;
    else if (Array.isArray(obj))
       return obj.map(o => cloneObjWithoutKeys(o, keys));

    return Object.fromEntries(
        Object.entries(obj)
              .filter(([k, v]) => !keys.includes(k))
              .map(([k, v]) => ([k, cloneObjWithoutKeys(v, keys)])
    ));
}

console.log(cloneObjWithoutKeys(item, removeKeys));
.as-console {background-color:black !important; color:lime;}
.as-console-wrapper {max-height:100% !important; top:0;}

Note, this will also traverse inner arrays of objects to filter out the desired keys.

Shidersz
  • 16,846
  • 2
  • 23
  • 48
0

You're calling the function recursively, but you don't do anything with the result this recursive call returns. You have to overwrite the sub-key with the filtered value:

let item = {
  "label": "test",
  "id": "test",
  "styles": {
    "label": "Styles",
    "styles": {
      "test": {
        "test": "test",
        "label": "test",
        "test1": {
          "label": "test",
          "image": {
            "label": "test",
            "type": "test",
            "value": "test",
            "autoSelect": "",
            "id": ""
          }
        }
      }
    }
  },
  "test": {
    "label": "test",
    "test": []
  }
}

const removeKeys = ["label", "type", "autoSelect"];

let filterObject = filterNestObject(item);

function filterNestObject(item) {
  return Object.keys(item)
    .filter(key => {
      if (typeof item[key] === 'object') {
        // set the key to the filtered result returned by the recursively called function
        item[key] = filterNestObject(item[key]);
      }

      if (!removeKeys.includes(key)) return true;

      return false
    })
    .reduce((object, key) => {
      return {
        ...object,
        [key]: item[key]
      };
    }, {});

}

console.log(filterNestObject(item));
Constantin Groß
  • 10,719
  • 4
  • 24
  • 50
0

I would probably use Object.entries, filter + includes, map, and Object.fromEntries -

const removeDeepKeys = (keys = [], o = {}) =>
  Object (o) === o
    ? Object
        .fromEntries
          ( Object
              .entries (o)
              .filter (([ k, _ ]) => ! keys .includes (k))
              .map (([ k, v ]) => [ k, removeDeepKeys (keys, v) ])
          )
    : o

Try it on your item -

removeDeepKeys ([ 'label', 'type', 'autoSelect' ], item)

Output -

{
  "id": "test",
  "styles": {
    "styles": {
      "test": {
        "test": "test",
        "test1": {
          "image": {
            "value": "test",
            "id": ""
          }
        }
      }
    }
  },
  "test": {
    "test": {}
  }
}

EDIT to support arrays -

const removeDeepKeys = (keys = [], o = {}) =>
  Array .isArray (o)
    ? o .map (v => removeKeys (keys, v))
    : Object (o) === o
        ? Object
            .fromEntries
              ( Object
                  .entries (o)
                  .filter (([ k, _ ]) => ! keys .includes (k))
                  .map (([ k, v ]) => [ k, removeDeepKeys (keys, v) ])
              )
        : o
Mulan
  • 129,518
  • 31
  • 228
  • 259