3

I have an object that looks like this:

var myObject = { a: { b: [{}], c: [{}, {d: 2}], e: 2, f: {} }, g:{}, h:[], i: [null, 2] }

I want to remove null values and and empty objects (array and objects) so that it looks like:

{ a: {c: [ {d: 2} ], e: 2 }, i: [ 2 ] }

The function should remove null values, empty objects and empty arrays. Any elegant way to do it ?

JLavoie
  • 16,678
  • 8
  • 33
  • 39
  • probably it is [a bad practice](https://jsfiddle.net/k4mor6gs/1/) uses regex. – Sphinx Sep 05 '18 at 00:51
  • 3
    I don't understand your expected output. If the input object has `a.c` as an array, why isn't the non-empty array preserved, that is, `{ a: { c: [{ d: 2 }], e: 2}, i: [2] }`? Why does the `i` array of `[null, 2]` disappear and then appear cleaned as a property of `g`? Why are there two `c` properties in the output (nested), whereas there's only ever one `c` property in the input? – CertainPerformance Sep 05 '18 at 01:24
  • Running your code in your answer produces `{ a: { c: [{ d: 2 }], e: 2}, i: [2] }`, as expected from the *problem description*, so I'm assuming that the desired output posted in the question just has typos? – CertainPerformance Sep 05 '18 at 01:43
  • I suspect there is a very elegant answer using ES6 syntax that would be 3 or 4 lines long. Hoping someone delivers. – ESR Sep 05 '18 at 03:53
  • @EdmundReed I'm pretty sure any solution will require manual recursion, explicit testing of objects vs arrays vs primitives (and nulls), and explicit checking of the number of remaining truthy keys - which is too much to do in only a few lines. My answer uses 15 *short* lines, it could be condensed into fewer, but that would compromise readability, which matters a whole lot more than code length. – CertainPerformance Sep 05 '18 at 23:09
  • There was a typo in my desired output. I made the correction. – JLavoie Sep 07 '18 at 00:41

3 Answers3

1

Here is a function that clean the object recursively. It will loop deeply through all the properties and remove null values, null arrays and null objects:

cleanUpObject(jsonObject: object): object {

    Object.keys(jsonObject).forEach(function (key, index) {
        const currentObj = jsonObject[key]

        if (_.isNull(currentObj)) {
            delete jsonObject[key]
        } else if (_.isObject(currentObj)) {
            if (_.isArray(currentObj)) {
                if (!currentObj.length) {
                    delete jsonObject[key]
                } else {
                    const cleanupArrayObj = []
                    for (const obj of currentObj) {
                        if (!_.isNull(obj)) {
                            const cleanObj = this.cleanUpJson(obj)
                            if (!_.isEmpty(cleanObj)) {
                                cleanupArrayObj.push(cleanObj)
                            }
                        }
                    }
                    if (!cleanupArrayObj.length) {
                        delete jsonObject[key]
                    } else {
                        jsonObject[key] = cleanupArrayObj
                    }
                }
            } else {
                if (_.isEmpty(Object.keys(jsonObject[key]))) {
                    delete jsonObject[key]
                } else {
                    jsonObject[key] = this.cleanUpJson(currentObj)

                    if (_.isEmpty(Object.keys(jsonObject[key]))) {
                        delete jsonObject[key]
                    }
                }
            }
        }
    }, this)

    return jsonObject
}
JLavoie
  • 16,678
  • 8
  • 33
  • 39
0

We don't know what you mean by clean, but from what I understand, you want to remove all null and empty values. This algorithm is straight-forward: recursively check for and remove any empty / null values (which are recursively checked).

function clean(obj) {
  // clean array
  if (Array.isArray(obj)) {
    for (let i=0; i<obj.length; i++) {
      if (isNothing(obj[i])) obj.splice(i, 1);  // remove value if falsy
      else if (typeof obj[i] === 'object') clean(obj[i]); // recurse if it's a truthy object
    }
    
  // clean other object
  } else {
    for (let prop in obj) {
      if (!obj.hasOwnProperty(prop)) continue;
      if (isNothing(obj[prop])) delete obj[prop]; // remove value if falsy
      else if (typeof obj[prop] === 'object') clean(obj[prop]); // recurse if it's a truthy object
    }
  }
}

// Recursively check for populated or nonnull content. If none found, return `true`. Recursive so [{}] will be treated as empty.
function isNothing(item) {
  // null / undefined
  if (item == null) return true;
  
  // deep object falsiness
  if (typeof item === 'object') {
    if (Array.isArray(item)) {
      // array -> check for populated/nonnull value
      for (let i=0; i<item.length; i++) {
        if (!isNothing(item[i])) return false;
      }
      return true;
    }
    // other object -> check for populated/nonnull value
    for (let prop in item) {
      if (!item.hasOwnProperty(prop)) continue;
      if (!isNothing(item[prop])) return false;
    }
    return true;
  }
  return false;
}

var myObject = { a: { b: [{}], c: [{}, {d: 2}], e: 2, f: {} }, g:{}, h:[], i: [null, 2] };

console.log("Before: " + JSON.stringify(myObject));
clean(myObject);
console.log("After: " + JSON.stringify(myObject));
clabe45
  • 2,354
  • 16
  • 27
-1

To reduce repetitive code, one option is to define a function (let's call it itemToBool) that can determine whether a generic value passed to it is truthy, or recursively truthy somewhere, should the value be an array or object. Then, in the function that gets passed the original object (or, gets recursively passed an object or array), you can call that itemToBool function whenever there's a value to validate.

In the case of arrays, map by itemToBool and then filter by Boolean. In the case of objects, reduce the entries of the object into another object: pass each value of the object through itemToBool to recursively transform it (in case the value is an array or object), and if the transformed value has any keys (or is a truthy primitive), assign it to the accumulator. No need to depend a library:

var myObject = {
  a: {
    b: [{}],
    c: [{}, {
      d: 2
    }],
    e: 2,
    f: {}
  },
  g: {},
  h: [],
  i: [null, 2]
};

// Returns a falsey value if the item is falsey,
// or if the deep cleaned array or object is empty:
const itemToBool = item => {
  if (typeof item !== 'object' || item === null) return item;
  const cleanedItem = clean(item);
  return Object.keys(cleanedItem).length !== 0 && cleanedItem;
};

const clean = obj => {
  if (Array.isArray(obj)) {
    const newArr = obj.map(itemToBool).filter(Boolean);
    return newArr.length && newArr;
  }
  const newObj = Object.entries(obj).reduce((a, [key, val]) => {
    const newVal = itemToBool(val);
    if (newVal) a[key] = newVal;
    return a;
  }, {});
  return Object.keys(newObj).length > 0 && newObj;
};

console.log(clean(myObject));

Hmm... you also might abstract the check of the number of keys into a function as well:

var myObject={a:{b:[{}],c:[{},{d:2}],e:2,f:{}},g:{},h:[],i:[null,2]}

// Returns the object / array if it has at least one key, else returns false:
const validObj = obj => Object.keys(obj).length && obj;
const itemToBool = item => (
  typeof item !== 'object' || item === null
  ? item
  : validObj(clean(item))
);
const clean = obj => validObj(
  Array.isArray(obj)
  ? obj.map(itemToBool).filter(Boolean)
  : Object.entries(obj).reduce((a, [key, val]) => {
      const newVal = itemToBool(val);
      if (newVal) a[key] = newVal;
      return a;
    }, {})
);

console.log(clean(myObject));
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320