Convert a complex nested JSON structure into an array of distinct objects without using indexes

I am faced with a task of flattening a simple nested object in order to insert it into my database efficiently.

const input = {
  name: "Benny",
  department: {
    section: "Technical",
    branch: {
      timezone: "UTC",
    },
  },
  company: [
    {
      name: "SAP",
      customers: ["Ford-1", "Nestle-1"],
    },
    {
      name: "SAP",
      customers: ["Ford-2", "Nestle-2"],
    },
  ],
};

The required format should include each array value as a separate sub-object stored within an array:

[
  {
    name: "Benny",
    "department.section": "Technical",
    "department.branch.timezone": "UTC",
    "company.name": "SAP",
    "company.customers": "Ford-1",
  },
  {
    name: "Benny",
    "department.section": "Technical",
    "department.branch.timezone": "UTC",
    "company.name": "SAP",
    "company.customers": "Nestle-1",
  },
  {
    name: "Benny",
    "department.section": "Technical",
    "department.branch.timezone": "UTC",
    "company.name": "SAP",
    "company.customers": "Ford-2",
  },
  {
    name: "Benny",
    "department.section": "Technical",
    "department.branch.timezone": "UTC",
    "company.name": "SAP",
    "company.customers": "Nestle-2",
  },
]

Compared to the undesired result where all fields are stored in a single object with indexes:

{
  name: 'Benny',
  'department.section': 'Technical',
  'department.branch.timezone': 'UTC',
  'company.0.name': 'SAP',
  'company.0.customers.0': 'Ford-1',
  'company.0.customers.1': 'Nestle-1',
  'company.1.name': 'SAP',
  'company.1.customers.0': 'Ford-2',
  'company.1.customers.1': 'Nestle-2'
}

Here is the code snippet I have been working on:

function flatten(obj) {
  let keys = {};
  for (let i in obj) {
    if (!obj.hasOwnProperty(i)) continue;
    if (typeof obj[i] == "object") {
      let flatObj = flatten(obj[i]);
      for (let j in flatObj) {
        if (!flatObj.hasOwnProperty(j)) continue;
        keys[i + "." + j] = flatObj[j];
      }
    } else {
      keys[i] = obj[i];
    }
  }
  return keys;
}

Thank you in advance!

Answer №1

You can transform the array's values into flat objects by taking them as part of a cartesian product.

const
    getArray = val => Array.isArray(val) ? val : [val],
    isObject = val => val && typeof val === 'object',
    getCartesian = obj => Object.entries(obj).reduce((res, [key, val]) => res.flatMap(item =>
        getArray(val).flatMap(w =>
            (isObject(w) ? getCartesian(w) : [w]).map(x => ({ ...item, [key]: x }))
        )
    ), [{}]),
    getFlat = o => Object.entries(o).flatMap(([key, val]) => isObject(val)
        ? getFlat(val).map(([l, val]) => [`${key}.${l}`, val])
        : [[key, val]]
    ),
    input = { name: "Benny", department: { section: "Technical", branch: { timezone: "UTC" } }, company: [{ name: "SAP", customers: ["Ford-1", "Nestle-1"] }, { name: "SAP", customers: ["Ford-2", "Nestle-2"] }] },
    result = getCartesian(input).map(o => Object.fromEntries(getFlat(o)));

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Answer №2

Revise

In the following code, I maintained your flatten function as is. Additionally, I introduced a new method called fix that transforms your original output into the desired format.

Please note: I altered the name value of the second occurrence of company to FOO.

const flatten = (obj) => {
  let keys = {};
  for (let i in obj) {
    if (!obj.hasOwnProperty(i)) continue;
    if (typeof obj[i] == 'object') {
      let flatObj = flatten(obj[i]);
      for (let j in flatObj) {
        if (!flatObj.hasOwnProperty(j)) continue;
        keys[i + '.' + j] = flatObj[j];
      }
    } else {
      keys[i] = obj[i];
    }
  }
  return keys;
};

const parseKey = (key) => [...key.matchAll(/(\w+)\.(\d)(?=\.?)/g)]
  .map(([match, key, index]) => ({ key, index }));

const fix = (obj) => {
  const results = [];
  Object.keys(obj).forEach((key) => {
    const pairs = parseKey(key);
    if (pairs.length > 1) {
      const result = {};
      Object.keys(obj).forEach((subKey) => {
        const subPairs = parseKey(subKey);
        let replacerKey;
        if (subPairs.length < 1) {
          replacerKey = subKey;
        } else {
          if (
            subPairs.length === 1 &&
            subPairs[0].index === pairs[0].index
          ) {
            replacerKey = subKey
              .replace(`\.${subPairs[0].index}`, '');
          }
          if (
            subPairs.length === 2 &&
            subPairs[0].index === pairs[0].index &&
            subPairs[1].index === pairs[1].index
          ) {
             replacerKey = subKey
              .replace(`\.${subPairs[0].index}`, '')
              .replace(`\.${subPairs[1].index}`, '');
             result[replacerKey] = obj[subKey];
          }
        }
        if (replacerKey) {
          result[replacerKey] = obj[subKey];
        }
      });
      results.push(result);
    }
  });
  return results;
};


const input = {
  name: "Benny",
  department: { section: "Technical", branch: { timezone: "UTC" } },
  company: [
    { name: "SAP", customers: ["Ford-1", "Nestle-1"] },
    { name: "FOO", customers: ["Ford-2", "Nestle-2"] },
  ]
};

const flat = flatten(input);
console.log(JSON.stringify(fix(flat), null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }

Initial solution

The most accurate method I could find to achieve your intended outcome is displayed below:

[
  {
    "name": "Benny",
    "department.section": "Technical",
    "department.branch.timezone": "UTC",
    "company.name": "SAP",
    "company.customers.0": "Ford-1",
    "company.customers.1": "Nestle-1"
  },
  {
    "name": "Benny",
    "department.section": "Technical",
    "department.branch.timezone": "UTC",
    "company.name": "SAP",
    "company.customers.0": "Ford-2",
    "company.customers.1": "Nestle-2"
  }
]

To obtain this result, I created a utility function named flattenBy which handles mapping the data based on a specific key such as company and then passes it to your existing flatten function (along with the current index).

const flatten = (obj, key, index) => {
  let keys = {};
  for (let i in obj) {
    if (!obj.hasOwnProperty(i)) continue;
    let ref = i !== key ? obj[i] : obj[i][index];
    if (typeof ref == 'object') {
      let flatObj = flatten(ref, key);
      for (let j in flatObj) {
        if (!flatObj.hasOwnProperty(j)) continue;
        keys[i + '.' + j] = flatObj[j];
      }
    } else { keys[i] = obj[i]; }
  }
  return keys;
}

const flattenBy = (obj, key) =>
  obj[key].map((item, index) => flatten(obj, key, index));


const input = {
  name: "Benny",
  department: { section: "Technical", branch: { timezone: "UTC" } },
  company: [
    { name: "SAP", customers: ["Ford-1", "Nestle-1"] },
    { name: "SAP", customers: ["Ford-2", "Nestle-2"] },
  ]
};

console.log(JSON.stringify(flattenBy(input, 'company'), null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }

Answer №3

This piece of code is designed to identify all forks, which are points where arrays exist indicating multiple potential versions of the input. It then creates a tree of permutations for each fork and ultimately applies a flattener to obtain the desired dot-delimited outcome.

Special Note: In this code snippet, h serves as a value holder. When h.s is set to 1 upon detecting the first fork, it essentially acts as a global variable across all calls to getFork on a given initial object. This ensures that only one fork is processed at a time while constructing a tree of forks.

const input = {"name":"Benny","department":{"section":"Technical","branch":{"timezone":"UTC"}},"company":[{"name":"SAP","customers":["Ford-1","Nestle-1"]},{"name":"SAP","customers":["Ford-2","Nestle-2"]},{"name":"BAZ","customers":["Maserati","x"],"SomeKey":["2","3"]}]}
    
const flatten = (o, prefix='') => Object.entries(o).flatMap(([k,v])=>v instanceof Object ? flatten(v, `${prefix}${k}.`) : [[`${prefix}${k}`,v]])
const findFork = o => Array.isArray(o) ? o.length : o instanceof Object && Object.values(o).map(findFork).find(i=>i)
const getFork = (o,i,h={s:0}) => o instanceof Object ? (Array.isArray(o) ? h.s ? o : (h.s=1) && o[i] : Object.fromEntries(Object.entries(o).map(([k,v])=>[k, getFork(v, i, h)]))) : o
const recurse = (o,n) => (n = findFork(o)) ? Array(n).fill(0).map((_,i)=>getFork(o, i)).flatMap(recurse) : o
const process = o => recurse(o).map(i=>Object.fromEntries(flatten(i)))
    
const result = process(input)
console.log(result)

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Switch from Index.html to Index.html#section1

Currently, I am working on a website and sending someone a draft for review. However, the Home screen is designed as a 'a href' link with "#home". The issue arises when the website opens from my computer; it goes to ...../Index.html instead of .. ...

Transferring information from Console to a web address

Every time I click on a button, the correct data appears in the console. However, now I want the data 'reid' to be passed to the URL when the button is clicked. When I attempt it using: this.router.navigateByUrl('/details/' + this.resu ...

Attempting to create a responsive Boxplot in Three.js

I created a basic three.js boxplot that loads individual jpg images for each mesh (bar). The boxplot appears fine on desktop, but is stretched/distorted on mobile devices. Is there a recommended approach to make it responsive? For simplicity, I will use e ...

Placing a dynamic loading image in the center of a webpage

Positioning an ajax loading image in the center of a mobile page. The code snippet above is used to load database records via ajax by displaying a loading image in a Div element. My inquiry is regarding how to ensure that this loading image appears at th ...

What alternative can be used instead of Document in Javascript when working with EJS?

Currently, I am utilizing ejs to handle my HTML tasks and I have come across an issue where I cannot use the usual document.getElementById('id') method within this environment. The error message displayed states "document not defined". This has ...

Maintain the highlighted background of the selected slot in react-big-calendar even after the selection process is completed

Currently, the default behavior is that when a slot is selected, it is highlighted in gray. However, once the selection is stopped, the highlighting disappears from the calendar fields. If we were to open a modal upon selection, this may not be necessary. ...

Refreshing the lightbox once new ajax content is loaded

While there are similar questions and answers related to my issue, I have not been able to apply them successfully. As a beginner in this area, any assistance is greatly appreciated. I am currently working with CubePortfolio, which is a jQuery-based, filt ...

Assign a Value to a Hidden Input Type When a User Submits a Form

I have a straightforward form set up in the following code. I am looking to add the value entered in the rm_accounts text box to the hidden page_confirm input value at the end of the URL. I hope that explanation is clear. In simple terms, if the user type ...

What happened? Node.js server abruptly terminated the connection?

I have a web app running on a Node.js server that needs to stay online continuously, so I am using forever. However, after some time, I encounter the following error: Error: Connection lost: The server closed the connection. at Protocol.end (/home/me/ ...

Accept two parameters for password change

Is there a way to include two values, one being the selected value and the other a PHP variable, in a dropdown select element's onChange function? For example: <div> <?php $sql4 = "SELECT DISTINCT (color), id ...

What are the keys that have the specified string in them?

Provided information includes : var people = [ { 'myKey': 'John Kenedy', 'status': 1 }, { 'myKey': 'Steeven Red', 'status': 0 }, { 'myKey': 'Mary_Kenedy', 's ...

Javascript - SecurityError: The action is not secure

Having this particular HTML element below: <a class="btn-modal" data-modal-window-id="category-add-modal" href="#" data-mode="edit" data-content="{&quot;category_id&quot;:16,&quot;category_name_en&quot;:&quot;Food&quot;,&quo ...

Objects hidden in the darkness, utilizing the traditional MeshPhongMaterial

Currently struggling with my shadows issue. Here is a snapshot of the problem I am encountering: https://i.sstatic.net/odnuE.png https://i.sstatic.net/RZitM.png The presence of those lines puzzles me, as the scene should be evenly illuminated! The shado ...

Experiencing a lack of content in an Express response

Currently utilizing express to manage a POST request, but encountering an issue when sending the body using node-fetch. After sending the body and logging it in express (server-side code), I am seeing an empty object. The reason behind this behavior remain ...

Interpreting an undefined HTTP GET request within a Node.js server

I am encountering an issue within my Node.js application. When I send an http get request through an ajax call on the client-side, the server-side does not recognize the request data and returns an "undefined" error message. This problem is puzzling to me ...

Attaching to directive parameters

I've been working on creating a draggable div with the ability to bind its location for further use. I'm aiming to have multiple draggable elements on the page. Currently, I've implemented a 'dragable' attribute directive that allo ...

How come my Bootstrap tooltip title only updates once when I use the keydown event in jQuery?

My goal is to provide users with a character count for text inputs and textareas based on a maximum limit. To achieve this, I have added tooltips to elements with a data-maxlength attribute. I update the tooltip for text inputs to display the current lengt ...

How can I show a loading screen while making a synchronous AJAX call in Chrome?

Is there any method to show a loading screen in Chrome while using async:false in an AJAX call? The use of setTimeout poses several challenges when making multiple synchronous AJAX calls within the setTimeout function. Additionally, the loading indicator ...

Error due to PlatformLocation's location dependency issue

My AppComponent relies on Location (from angular2/router) as a dependency. Within the AppComponent, I am using Location.path(). However, when running my Jasmine test, I encountered an error. Can you help me identify the issue with my Jasmine test and guide ...

The function for converting a jQuery element to a DOM element is yielding a blank string as

What is the reason behind this code not working as expected: function getElementWidth(el){ return $(el)[0].style.width }; getElementWidth('#someElementIdId'); // returns -> "" However, when using this code... function getElementWidth(el){ ...