Search code examples
javascriptjsonparsingnested

JavaScript create a JSON Object from nested flat keys


The object I must parse:

const object = {
"item1": "value",
"item2/0/subitem1": "value",
"item2/0/subitem2": "value",
"item2/1/subitem1": "value",
"item2/1/subitem2": "value",
"item3/0/subitem1/subsubitem1": "value",
"item3/1/subitem1/subsubitem1": "value",
"item4/0/subitem1/0/subsubitem1": "value",
"item4/0/subitem1/1/subsubitem1": "value",
}

I would like to parse it into a nested object that looks like this:

const parsedObject = {
item1: value,
item2: [
  {
    subitem1: value,
    subitem2: value,
  },
  {
    subitem1: value,
    subitem2: value,
  }
],
item3: [
  {
    subtiem1: {
      subsubitem1: value,
    }
  },
  {
    subtiem1: {
      subsubitem1: value,
    }
  },
],
item4: [
  {
    subitem1: [
      {
        subsubitem1: value,
      },
      {
        subsubitem1: value,
      },
    ]
  }
]

}

I have tried to write this function, but I'm stuck and do not know how to continue with it.

const object = {
  "item1": "value",
  "item2/0/subitem1": "value",
  "item2/0/subitem2": "value",
  "item2/1/subitem1": "value",
  "item2/1/subitem2": "value",
  "item3/0/subitem1/subsubitem1": "value",
  "item3/1/subitem1/subsubitem1": "value",
  "item4/0/subitem1/0/subsubitem1": "value",
  "item4/0/subitem1/1/subsubitem1": "value",
}

const mainFunction = () => {
  const specie = {};
  const map = new Map();
  Object.keys(object).forEach((key) => {
    const keyTokens = key.split('/');
    processKeyValues(specie, keyTokens, object[key], map);
  });
  console.log(specie);
}

const convertStringToBoolean = (str: string): string | boolean => {
  return str.toLowerCase() === 'true' ? true : str.toLowerCase() === 'false' ? false : str;
};

const processKeyValues = (specie: any, keyTokens: any[], value: string, map: Map < any, any > ): any => {
  keyTokens.reduce((prev, curr, index, array) => {
    if (index === array.length - 1) {
      const valuePossibleNumber = parseInt(value); //transform number strings into numbers
      if (!isNaN(valuePossibleNumber)) {
        prev[curr] = valuePossibleNumber;
        return;
      }
      prev[curr] = convertStringToBoolean(value); //boolean strings into booleans
      return;
    }
    const currentKeyPossibleNumber = parseInt(curr);
    const currentKeyIsNumber = !isNaN(currentKeyPossibleNumber);
    const nextKeyPossibleNumber = parseInt(array[index + 1]);
    const nextKeyIsNumber = !isNaN(nextKeyPossibleNumber);

    if (currentKeyIsNumber) {
      const hasMappedIndex = map.has(currentKeyPossibleNumber);
      let mappedIndex = map.get(currentKeyPossibleNumber);
      if (!hasMappedIndex) {
        mappedIndex = map.size;
        map.set(currentKeyPossibleNumber, mappedIndex);
      }
      const currentKeyAsIndexDoesNotExist = !prev[mappedIndex];
      if (currentKeyAsIndexDoesNotExist) {
        if (nextKeyIsNumber) {
          prev.push([]);
        } else {
          prev.push({});
        }
      }
      return prev[mappedIndex];
    }
    const currentKeyDoesNotExist = !prev[curr];
    if (currentKeyDoesNotExist) {
      if (nextKeyIsNumber) {
        prev[curr] = [];
        map.clear();
      } else {
        prev[curr] = {};
      }
    }
    return prev[curr];
  }, specie);
};

The reason I use a map for the array indexes is that it may be that I do not receive those indexes necessarily in order like 0,1,2 etc., but also like 0,1,10,101 etc. and I access that index in the array I am creating because it is not that large.

Seeing that I am receiving these objects from an endpoint I cannot modify, it would be ideal if I could find a way to generalize the code to cover situations when I could get multiple nested arrays ans objects so I thought a recursive function would be best suited, but I cannot figure out how, thus I would be thankful if you could help me out.


Solution

  • I'll try again. Lets re-use that solution. It might not be the shortest way, but why not.

    Here is the old one: Check out the output.

    const object = {
      "item1": "value",
      "item2/0/subitem1": "value",
      "item2/0/subitem2": "value",
      "item2/1/subitem1": "value",
      "item2/1/subitem2": "value",
      "item3/0/subitem1/subsubitem1": "value",
      "item3/1/subitem1/subsubitem1": "value",
      "item4/0/subitem1/0/subsubitem1": "value",
      "item4/0/subitem1/1/subsubitem1": "value",
    }
    
    let agg = {
      temp: []
    };
    
    Object.entries(object).forEach(([path, value]) => {
      path.split('/').reduce((agg, part, level, parts) => {
        if (!agg[part]) {
          agg[part] = {
            temp: []
          };
          agg.temp.push({
            id: parts.slice(0, level + 1).join("/"),
            level: level + 1,
            children: agg[part].temp
          })
          // console.log(agg)
        }
        return agg[part];
      }, agg)
    })
    
    var result = agg.temp;
    console.log(result)
    .as-console-wrapper {
      max-height: 100% !important
    }

    Now we want to transform that output into the expected output. This is done using a recursion (arr_tree_to_obj) which works by iterating entire intermediate tree and creating the proper key: value pair on the target object.

    const object = {
      "item1": "value",
      "item2/0/subitem1": "value",
      "item2/0/subitem2": "value",
      "item2/1/subitem1": "value",
      "item2/1/subitem2": "value",
      "item3/0/subitem1/subsubitem1": "value",
      "item3/1/subitem1/subsubitem1": "value",
      "item4/0/subitem1/0/subsubitem1": "value",
      "item4/0/subitem1/1/subsubitem1": "value",
    }
    
    function flat_directory_list_to_directory_tree(list) {
      let agg = {
        temp: []
      };
    
      Object.entries(object).forEach(([path, value]) => {
        path.split('/').reduce((agg, part) => {
          if (!agg[part]) {
            agg[part] = {
              temp: []
            };
            agg.temp.push({
              id: part,
              value: value,
              children: agg[part].temp
            })
          }
          return agg[part];
        }, agg)
      })
    
      var mid = agg.temp;
      // console.log(mid)
    
      function arr_tree_to_obj(arr) {
        var result = arr.reduce(function(agg, item) {
          if (!item.children.length) {
            agg[item.id] = item.value
          } else {
            agg[item.id] = arr_tree_to_obj(item.children);
          }
          return agg;
        }, {})
        return result;
      }
    
      return (arr_tree_to_obj(mid))
    }
    
    var parsedObject = flat_directory_list_to_directory_tree(object);
    console.log(parsedObject)
    .as-console-wrapper {
      max-height: 100% !important
    }

    Update: Lets transform all objects with numeric keys into an array.

    const object = {
      "item1": "value",
      "item2/0/subitem1": "value",
      "item2/0/subitem2": "value",
      "item2/1/subitem1": "value",
      "item2/1/subitem2": "value",
      "item3/0/subitem1/subsubitem1": "value",
      "item3/1/subitem1/subsubitem1": "value",
      "item4/0/subitem1/0/subsubitem1": "value",
      "item4/0/subitem1/1/subsubitem1": "value",
    }
    
    function flat_directory_list_to_directory_tree(list) {
      let agg = {
        temp: []
      };
    
      Object.entries(object).forEach(([path, value]) => {
        path.split('/').reduce((agg, part) => {
          if (!agg[part]) {
            agg[part] = {
              temp: []
            };
            agg.temp.push({
              id: part,
              value: value,
              children: agg[part].temp
            })
          }
          return agg[part];
        }, agg)
      })
    
      var mid = agg.temp;
      // console.log(mid)
    
      function arr_tree_to_obj(arr) {
        var result = arr.reduce(function(agg, item) {
          if (!item.children.length) {
            agg[item.id] = item.value
          } else {
            agg[item.id] = arr_tree_to_obj(item.children);
          }
          return agg;
        }, {})
        return result;
      }
    
      var mid2 = (arr_tree_to_obj(mid))
      // console.log(mid2)
      
      function convert_possibly_object_to_array(obj) {
        if (typeof obj !== "object") {
          return obj;
        }
        if (Array.isArray(obj)) {
          return obj;
        }
        var keys = Object.keys(obj);
        if (keys.every(function(key) {
            return Number.isInteger(+key)
          })) {
          return Object.values(obj)
        }
        return obj;
      }
    
    
      function iterate2(obj) {
        Object.keys(obj).forEach(function(key) {
          var value = obj[key];
          obj[key] = convert_possibly_object_to_array(value)
          if (typeof value == "object" && value !== null) {
            iterate2(value)
          }
        })
      }
    
      iterate2(mid2)
      return mid2;
    }
    
    var parsedObject = flat_directory_list_to_directory_tree(object);
    console.log(parsedObject)
    .as-console-wrapper {
      max-height: 100% !important
    }

    To summarize what this functions does is gets a list of paths or directories and converts it to a trees of three types.

    First, makes it a tree with of nodes of type : {id, value, children}

    Then converts it to an object whose properties have values or are objects of themselves. This is the classic representation of a tree as a nested object.

    Finally, it convert that object and corrects that every object with numeric keys shall become an array. Which is the desired output hopefully