2
votes

I have this Flattened Object

{
  "abc.def.ghi": "foo",
  "abc.def.jkl": "bar"
}

I want to write a dataweave to convert it into the original object, i.e.

{
  "abc": {
    "def": {
      "ghi": "foo",
      "jkl": "bar"
    }
  }
}

I am trying to avoid hard coding the keys because it is a quite large object, So I do not want something like this:

%dw 2.0
var test = {
    "abc.def.ghi": "foo",
    "abc.def.jkl": "bar"
}
output application/json
---
{
    abc: {
        def: {
            ghi: test."abc.def.ghi",
            jkl: test."abc.def.jkl"
        }
    }
}

Can I use some combination of available dataweave functions for achieving this?

Here is what I have tried so far:

%dw 2.0
var test = {
    "abc.def.ghi": "foo",
    "abc.def.jkl": "bar"
}
output application/json
---
test mapObject ((value, key) -> 
    (key as String splitBy  ".")[-1 to 0]  
         reduce ((item, acc = value) -> 
             (item): acc     
/*
   First item=ghi,acc=foo => acc = {ghi: "foo"} 
   next item=def, acc={ghi: "foo"} => acc={def:{ghi:"foo"}}
*/
    )
)

But this would generate some kind of separate pair of the nested JSON. Here is the output of the above code:

{
  "abc": {
    "def": {
      "ghi": "foo"
    }
  },
  "abc": {
    "def": {
      "jkl": "bar"
    }
  }
}
3

3 Answers

3
votes

This is similar to @olamiral solution, but simplified and supports arrays.

%dw 2.0
output application/json

// Creates a array of key-value tuples with the object structure. 
// I was not able to use entriesOf() because I had to modify the key to split it
var tuples = payload pluck ((value, key, index) -> 
    { 
        k: key splitBy("."), 
        v: value}
    )

// Using groupBy, group the childs and maps to an object structure.
fun flatToObject(tuples, index) =
    (tuples groupBy $.k[index]) mapObject ((groupedTuples, key, idx) -> 
        if(groupedTuples[0].k[index + 1]?) 
            // Has more levels
            { (key): flatToObject(groupedTuples,index + 1) }
        else 
            // It's a leaf
            { (key): if (sizeOf(groupedTuples.v) > 1)
                    // It has an array of values
                    groupedTuples.v 
                else 
                    // It has a single value
                    groupedTuples.v[0]
            }
    )
---
flatToObject(tuples,0)

With this payload:

{
    "abc.def.ghi": "foo",
    "abc.def.jkl": "bar",
    "abc.de.f": "bar2",
    "abc.def.jkm": "bar3",
    "abc.de.f": 45
}

It's producing this output:

{
  "abc": {
    "def": {
      "ghi": "foo",
      "jkl": "bar",
      "jkm": "bar3"
    },
    "de": { 
      "f": ["bar2", 45]
    }
  }
}

This solution does not support mixing simple values and objects in the same array.

4
votes

You can try the following dataweave expression, based on this Apisero article, written by Mohammad Mazhar Ansari: https://apisero.com/property-to-yaml-file-conversion-using-mulesoft/

%dw 2.0
var test = {
    "abc.def.ghi": "foo",
    "abc.def.jkl": "bar"
}
import * from dw::core::Strings
output application/json
fun generateArray (obj) = obj pluck (v, k) -> (k): v
fun isSubChildExists (key) = (((key) splitBy ("."))[1] != null)
fun propToJSON(key, value) = if (isSubChildExists(key)) {
   (substringBefore(key, ".")) : propToJSON(substringAfter(key, "."), value)
}
else
   (key): value
fun arrToObj(arr) = arr reduce ((env, obj={}) -> obj ++ env)
fun CombineObjBasedOnKey (Obj) =
if (typeOf(Obj) == Array and sizeOf(Obj..) > 1)
((Obj groupBy (item, index) -> keysOf(item)[0]) mapObject ((value, key, index) ->
   (if (typeOf(value) == Array)
       (key): CombineObjBasedOnKey(value..'$key')
   else if (typeOf(value) == String)
       value
   else
       (key): value
) as Object))
else
   Obj[0]
---
CombineObjBasedOnKey(generateArray(test) map ((item, index) -> item mapObject ((value, key, index) -> propToJSON((key), value))
))

Output:

{
  "abc": {
    "def": {
      "ghi": "foo",
      "jkl": "bar"
    }
  }
}
3
votes

Another way to do this is to use the update operator that was introduce in 4.3. With this operator we can do upsert (insert the value when is not present or update if present) Using that and a reduce we can go from each of the part of the expression and do the proper update

%dw 2.0
output application/json
import * from dw::util::Values


fun upsert(object: {}, path:Array<String>, value: Any): Object = do {
    path match {
        case [] -> object
        case [x ~ xs] -> 
                if(isEmpty(xs))
                    object update  {
                        case ."$(x)"! -> value                                
                    }
                else
                    object update  {
                        case selected at ."$(x)"! -> 
                            //selected is going to be null when the value is not present  
                            upsert(selected default {}, xs, value)
                    }  
    }
}
---
payload 
    pluck ((value, key, index) -> {key: key, value: value})
    reduce ((item, resultObject = {} ) -> do {
        upsert(resultObject, (item.key as String splitBy '.') , item.value)
    })