2

I have this simple nested object which I need to flatten to be able to insert it into my database.

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 desired result is like this, each value in the arrays results in a new sub-object stored in 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",
  },
]

Instead of the result below which all fields stored in 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'
}

My code looks like this:

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;
}

Thanks in advance!

Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
Benny
  • 488
  • 5
  • 18
  • 2
    your wanted result is not possible, because you have at least two same named properties, likem `company.customers`, which is not possible. – Nina Scholz Jan 25 '23 at 18:45
  • 2
    In your "desired result", you cannot have duplicate keys e.g. `company.customers`. This desired output is invalid. – Mr. Polywhirl Jan 25 '23 at 18:45

3 Answers3

2

Edit

In the code below, I left your flatten functionality the same. I added a fix method that converts your original output into your desired output.

Note: I changed the name value of second 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; }

Original response

The closest (legitimate) I could get to your desired result is:

[
  {
    "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"
  }
]

I had to create a wrapper function called flattenBy that handles mapping the data by a particular key e.g. company and passes it down to your 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; }
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
2

You could take the array's values as part of a cartesian product and get finally flat objects.

const
    getArray = v => Array.isArray(v) ? v : [v],
    isObject = v => v && typeof v === 'object',
    getCartesian = object => Object.entries(object).reduce((r, [k, v]) => r.flatMap(s =>
        getArray(v).flatMap(w =>
            (isObject(w) ? getCartesian(w) : [w]).map(x => ({ ...s, [k]: x }))
        )
    ), [{}]),
    getFlat = o => Object.entries(o).flatMap(([k, v]) => isObject(v)
        ? getFlat(v).map(([l, v]) => [`${k}.${l}`, v])
        : [[k, v]]
    ),
    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; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • Hi, thank you for the answer, I know for sure that I don't have nested arrays. Is there something to do to get rid of the indexes? – Benny Jan 25 '23 at 19:23
  • what do you want instead? same named properties are not possible in objects. – Nina Scholz Jan 25 '23 at 19:28
  • instead of indexes, another object in the final array. is that possible? – Benny Jan 25 '23 at 19:29
  • yes, it is. you need kind of a cartesian product. – Nina Scholz Jan 25 '23 at 19:30
  • Currently I have no idea how I can do such a thing. I've found python code that do it: https://stackoverflow.com/a/57793454/8992710. But it's python :( – Benny Jan 25 '23 at 19:37
  • Thank you Nina! I think there is a little bug, when company.customers array is empty `[]` the whole object is removed from the output. Can you do something about it? – Benny Jan 26 '23 at 04:45
  • what would you like to get in this case? – Nina Scholz Jan 26 '23 at 08:09
  • Nina thank you so much! I've added simple helper function before the execution of yours and it works just fine! I'll write some tests for the whole code and post my final solution. Thank you for your time, appreciate it alot! – Benny Jan 26 '23 at 13:15
1

This code locates all forks (places where arrays are located which indicate multiple possible versions of the input), and constructs a tree of permutations of the input for each fork. Finally, it runs all permutations through a flattener to get the desired dot-delimited result.

Note: h is a value holder, where h.s is set to 1 as soon as the first fork is found. This acts like a kind of global variable across all invocations of getFork on a particular initial object, and forces only one fork to be considered at a time when building up 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)
Andrew Parks
  • 6,358
  • 2
  • 12
  • 27
  • Hi! Thank you so much for your time, it works fine! Just a thing that I notice: If I change the value of the customers array, let's say empty array instead of [Ford-1, Nestle-1] the code output is []. I mean there is no output for the other objects – Benny Jan 25 '23 at 21:33
  • @Benny you're right, there was a bug. I've fixed it, with a new and simpler approach – Andrew Parks Jan 26 '23 at 00:33
  • I'm testing the code and seems like it works better, I get incorrect result if I add this object inside company array: `{"name":"BAZ","customers":["Maserati", "x"], "Somekey":["2", "3"]}`. is there something you can do about it? Thanks! – Benny Jan 26 '23 at 04:19
  • @Benny good point, the old version did not allow multiple forks at the same level. It should work now – Andrew Parks Jan 26 '23 at 05:28
  • Hi Andrew, Thank you for your time and for the solution. Appreciate it! – Benny Jan 26 '23 at 13:10