Search code examples
muledataweave

Transform Multipart format data to nested JSON


I am trying to convert multipart format data to JSON dynamically

Input Data

----------------------------439270980986078404603032
Content-Disposition: form-data; name="key1"

value1
----------------------------439270980986078404603032
Content-Disposition: form-data; name="key2[name]"

name
----------------------------439270980986078404603032
Content-Disposition: form-data; name="key2[phone]"

phone
----------------------------439270980986078404603032
Content-Disposition: form-data; name="key2[location]"


----------------------------439270980986078404603032
Content-Disposition: form-data; name="key2[vaid]"

true
----------------------------439270980986078404603032
Content-Disposition: form-data; name="key3"


----------------------------439270980986078404603032
Content-Disposition: form-data; name="key4"

value4
----------------------------439270980986078404603032
Content-Disposition: form-data; name="key2[address][city]"

xyz
----------------------------439270980986078404603032
Content-Disposition: form-data; name="key2[address][state]"

abc
----------------------------439270980986078404603032
Content-Disposition: form-data; name="key2[address][code]"

5678
----------------------------439270980986078404603032
Content-Disposition: form-data; name="key2[address][country]"

US
----------------------------439270980986078404603032--

Expected Output

{
    "key1": "value1",
    "key2": {
        "name": "name",
        "phone": "phone",
        "location": null,
        "valid": true,
        "address": {
            "city": "xyz",
            "state": "abc",
            "code": 5678,
            "country": "US"
        }
    },
    "key3": null,
    "key4": "value4"

}

DataWeave I tried

%dw 2.0
output application/json
---
{
    "key1": payload.parts.key1.content,
    "key2": {
        "name": payload.parts."key2[name]".content,
        "phone": payload.parts."key2[phone]".content,
        "location": payload.parts."key2[location]".content,
        "valid": payload.parts."key2[vaid]".content,
        "address": {
            "city": payload.parts."key2[address][city]".content,
            "state": payload.parts."key2[address][state]".content,
            "code": payload.parts."key2[address][code]".content,
            "country": payload.parts."key2[address][country]".content
        }
    },
    "key3": payload.parts.key3.content,
    "key4": payload.parts.key4.content
}

The above DataWeave although gives expected output but it wont work for all type of scenarios like if we add or remove keys from payload. A dynamic solution would be more preferably in my case. Also for eg a scenario is there where name = key2[address][state][county] that also needs to be handled dynamically.


Solution

  • There are two parts to this problem: convert the multipart input to something usable for the transformation and to convert that intermediate result that has sub index expressions into a nested object before outputting JSON. The second part is the hard one but luckily that was previously asked and there are already previous answers on how to do that.

    For the first part I'll just get the parts of the multipart and discard everything else:

    payload.parts 
        mapObject ((value, key, index) -> (key): value.content)
    

    Then I'll reuse the previous answer that creates nested objects from keys in format "abc.def.ghi". Because this problem uses keys in format "key2[address][city]" I used a small utility function to replace the combinations of braces by dots in the key names:

    fun convertKeys(s)=s replace "][" with "." replace "[" with "." replace "]" with "."    
    

    The full script is:

    %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)
                        }  
        }
    }
    
    fun groupSubelements(x)=x pluck ((value, key, index) -> {key: key, value: value})
        reduce ((item, resultObject = {} ) -> do {
            upsert(resultObject, (item.key as String splitBy '.') , item.value)
        })
    
    fun convertKeys(s)=s replace "][" with "." replace "[" with "." replace "]" with "."    
    ---
    groupSubelements(payload.parts 
        mapObject ((value, key, index) -> (convertKeys(key as String)): value.content))
    

    Output:

    {
      "key1": "value1",
      "key2": {
        "name": "name",
        "phone": "phone",
        "location": "",
        "vaid": "true",
        "address": {
          "city": "xyz",
          "state": "5678",
          "country": "US"
        }
      },
      "key3": "",
      "key4": "value4"
    }
    

    The parts contents that are empty are returned as empty strings. This is how DataWeave is treating those entries. Your expected output is to have them as null. If that needed you could change the expression value.content to add an empty check and return null instead: if (isEmpty(value.content)) null else value.content