Search code examples
javascriptobjectrecursiondata-structuresreduce

How does one transform a nested object's hierarchy into a flat representation of itself with each flattened key reflecting its original property path?


I have a JSON conform object structure like this ...

{
  "Name": "Pedro Smith",
  "Age": "8",
  "Parents": {
    "Mother": {
      "Name": "Maria Smith",
      "RG": "123456"
    },
    "Father": {
      "Name": "Jose Smith",
      "RG": "5431"
    }
  }
}

... and I want to process the above object into a flat representation of itself. The new object should not be nested but its former hierarchy should be reflected by its key/property names where each new key was aggregated by its deepest value's property path.

The expected structure of the above given example should look like this ...

{
  "Name": "Pedro Smith",
  "Age": "8",
  "Parents-Mother-Name": "Maria Smith",
  "Parents-Mother-RG": "123456",
  "Parents-Father-Name": "Jose Smith",
  "Parents-Father-RG": "5431"
}

How can such a transformation task be achieved in JavaScript?


Solution

  • TLDR

    • The final, recursion based, approach is presented with the last example.
    • Since the OP seems to be new to the matter, there will be 3 code iteration steps, each with explanations and documentation links, towards the final solution.

    If one starts thinking about a solution for a non nested object structure or how to retrieve the object's first level key-value pairs (aka entries) one might think, that iterating over such entries and somehow executing a logging forEach key-value pair might be a valid first step/approach.

    And since one does not want to use something like for...in but something more readable, an example code might end up looking like this ...

    const sample = {
      Name: "Pedro Smith",
      Age: "8",
      Parents: {
        Mother: {
          Name: "Maria Smith",
          RG: "123456",
        },
        Father: {
          Name: "Jose Smith",
          RG: "5431",
        },
      },
    };
    
    Object
      .entries(sample)
      .forEach(entry => {
    
        const key = entry[0];
        const value = entry[1];
    
        console.log(key + ': ' + value + '\n');
      });
    .as-console-wrapper { min-height: 100%!important; top: 0; }

    Of cause one can improve the above code by not logging every iteration step but by aggregating the final result, which could be achieved with e.g. the help of reduce, join and some Destructuring assignments ...

    const sample = {
      Name: "Pedro Smith",
      Age: "8",
      Parents: {
        Mother: {
          Name: "Maria Smith",
          RG: "123456",
        },
        Father: {
          Name: "Jose Smith",
          RG: "5431",
        },
      },
    };
    
    console.log(
      Object
        .entries(sample)
        .reduce((result, [key, value]) => [
    
          result,
          key + ': ' + value,
        
        ].join('\n'), '')
    );
    .as-console-wrapper { min-height: 100%!important; top: 0; }

    But the OP wants both a flattened object structure and aggregated keys, where the latter is supposed to somehow reflect the key path into each similar formed substructure. Thus, one needs to walk the object tree/structure.

    In order to do so, and since the above example already offers a solution for a simple object branch, one needs to turn the reduce function into a recursive one.

    The aggregating/accumulating first argument of reduce will be used for collecting and configuration purposes like not just carrying the result reference but also referring to the current's key (aggregated by a Template literal) prefix and the connector string ...

    function recursivelyConcatKeyAndAssignFlatEntry(collector, [key, value]) {
      const { connector, keyPath, result } = collector;
    
      // concatenate/aggregate the current
      // but flattened key if necessary.
      key = keyPath
        ? `${ keyPath }${ connector }${ key }`
        : key;
    
      if (value && (typeof value === 'object')) {
        // ... recursively continue flattening in case
        // the current `value` is an "object" type ...
        Object
          .entries(value)
          .reduce(recursivelyConcatKeyAndAssignFlatEntry, {
            connector,
            keyPath: key,
            result,
          });
      } else {
        // ... otherwise ... just assign a new flattened
        // key and current value pair to the final object.
        result[key] = value;
      }
      return collector;
    }
    
    const sample = {
      Name: "Pedro Smith",
      Age: "8",
      Parents: {
        Mother: {
          Name: "Maria Smith",
          RG: "123456",
        },
        Father: {
          Name: "Jose Smith",
          RG: "5431",
        },
      },
    };
    
    console.log(
      Object
        .entries(sample)
        .reduce(recursivelyConcatKeyAndAssignFlatEntry, {
    
          connector: '-',
          keyPath: '',
          result: {},
    
        }).result
    );
    .as-console-wrapper { min-height: 100%!important; top: 0; }