(Recursion) How to get all key/value pairs from an Object with nested objects and arrays

Tags: , , , ,



I have an object that look like this:

        {
            "id": 45745049
            "seller": {
                "first_name": "Sam",
                "last_name": "Smith",
                "email": "samsmith@smith.com",
                "phone": {
                    "number": "1111-1111",
                    "verified": false
                },
            },
            "order_items": [
                {
                    "item": {
                        "id": "29239765",
                        "title": "item1",
                        "colors": [
                             "red",
                             "green",
                             "blue"
                        ]
                    },
                    "quantity": 1,
                    "unit_price": 230,
                },
                {
                    "item": {
                        "id": "238457363",
                        "title": "item2",
                        "colors": [
                             "red"
                        ]
                    },
                    "quantity": 2,
                    "unit_price": 110,
                }
            ],
            "date_created": "2020-08-03T12:17:25.000-04:00",
            "date_last_updated": "2020-08-03T16:51:35.61Z"
        }

I want an array with pairs of EVERY key in the object with the value.

For example:

[
  ["id", 45745049], 
  ["first_name", "Sam"], 
  ....., 
  ["phone.number", "1111-1111"], 
  ["phone.verified", false], 
  ....etc
]

Everything Ok until that point. The problem is when a property is an array of objects. The output I want is the following:

[
   ..., 
   ["order_items1.item.id", 29239765], 
   ["order_items1.item.colors1", "red"], 
   ["order_items1.item.colors2", "green"], 
   ..., 
   ["order_items2.item.id", 238457363], 
   ["order_items2.item.colors1", "red"], 
   ...etc
]

So it needs to check if the property is an array and add the position number if so.

I know I need a recursive function but I dont know how to do it. This is what I got until now.

getObjectKeys = (obj) => {
    let FieldName = "";
    let FieldValue = "";
    for(var prop in obj) {
        FieldName += prop;
        if(!(prop instanceof Array) && (typeof prop !== "object") && obj[prop]) {
            FieldValue = obj[prop];
        } else if(prop instanceof Array && prop.length !== 0){
            prop.forEach((innerItem, i) => {
                FieldName += `${i+1}.`;
                // check if the inner item is an array or whatever an do it all again
                // Dont know what to do here.                   
            });
        } else {
            getObjectKeys(obj[prop]);
        }        
    }
    return [FieldName, FieldValue];
    
}

Note: I dont want the empty or null keys.

I would be very grateful if someone can help me. Thanks anyways!

Answer

This does something very similar to what you’re looking for. It’s a technique I use often.

const getPaths = (obj) =>
  Object (obj) === obj
    ? Object .entries (obj) .flatMap (([k, v]) => getPaths (v) .map (p => [k, ... p]))
    : [[]]

const path = (ps) => (obj) => 
  ps .reduce ((o, p) => (o || {}) [p], obj)

const flatten = (obj) => 
  Object .fromEntries (getPaths (obj) .map (p => [p.join('.'), path (p) (obj)]))

const input = {id: 45745049, seller: {first_name: "Sam", last_name: "Smith", email: "samsmith@smith.com", phone: {number: "1111-1111", verified: false}}, order_items: [{item: {id: "29239765", title: "item1", colors: ["red", "green", "blue"]}, quantity: 1, unit_price: 230}, {item: {id: "238457363", title: "item2", colors: ["red"]}, quantity: 2, unit_price: 110}], date_created: "2020-08-03T12: 17: 25.000-04: 00", date_last_updated: "2020-08-03T16: 51: 35.61Z"}

console .log (flatten (input))
.as-console-wrapper {min-height: 100% !important; top: 0}

The differences are that there is a separator before the array index and that I use zero-based arrays, not one-based arrays.

I would suggest that it’s a much better output format. If nothing else, it would probably allow you to rehydrate the original format. But if you want to change it, you should probably simply reduce the path to combine the numeric elements with their predecessors, something like:

const flatten = (obj) => 
  Object .fromEntries (getPaths (obj) .map (p => [
    p .reduce (
      (a, k) => /^d+$/ .test(k) ? [...a .slice (0, -1), a [a .length - 1] + (1 + (+k))] : [...a, k], 
      []
    ) .join ('.'), 
    path2 (p) (obj)
  ]))

But this would require changes if the outer object might be an array.

Again, though, absent a very good reason to use your requested format, I would strongly recommend my alternative.



Source: stackoverflow