Search code examples
jsongroovycoding-efficiencydynamic-code

Dynamically Parsing JSON with Groovy


I have a JSON document pulled back from a support system API. With my code, I want to pull out the pre-configured fields dynamically, presuming that the JSON may have more or fewer of the desired fields when my program calls the API.

I have some code that works, though it seems very convoluted and inefficient.

Here is a snippet of the pieces of JSON that I'm interested in:

{
   "rows": [
      {
         "assignee_id": 1,
         "created": "2017-01-25T14:13:19Z",
         "custom_fields": [],
         "fields": [],
         "group_id": 2468,
         "priority": "Low",
         "requester_id": 2,
         "status": "Open",
         "subject": "Support request",
         "ticket": {
            "description": "Ticket descritpion",
            "id": 1000,
            "last_comment": {
               "author_id": 2,
               "body": "Arbitrary text",
               "created_at": "2017-02-09T14:21:38Z",
               "public": false
            },
            "priority": "low",
            "status": "open",
            "subject": "Support request",
            "type": "incident",
            "url": "Arbitrary URL"
         },
         "updated": "2017-02-09T14:21:38Z",
         "updated_by_type": "Agent"
      },
      {
         "assignee_id": 1,
         "created": "2017-02-09T14:00:18Z",
         "custom_fields": [],
         "fields": [],
         "group_id": 3579,
         "priority": "Normal",
         "requester_id": 15,
         "status": "Open",
         "subject": "Change request",
         "ticket": {
            "description": "I want to change this...",
            "id": 1001,
            "last_comment": {
               "author_id": 20,
               "body": "I want to change the CSS on my website",
               "created_at": "2017-02-09T14:12:12Z",
               "public": true
            },
            "priority": "normal",
            "status": "open",
            "subject": "Change request",
            "type": "incident",
            "url": "Arbitrary URL"
         },
         "updated": "2017-02-09T14:12:12Z",
         "updated_by_type": "Agent"
      }
   ]
}

I have an ArrayList called wantedFields that I build up from a config to define which information I want to pull out from the JSON:

  ["id","subject","requester_id","status","priority","updated","url"]

The complexity is that data is replicated in the API, and I only want to pull out data once, with a preference for the data in "rows" where applicable. My method for doing this is below. It feels like I'm repeating code but I can't really see how to make this work more efficiently. The JSON is held as "viewAsJson".

def ArrayList<Map<String,Object>> assignConfiguredFields(viewAsJson, wantedFields) {
    //Pull out configured fields from JSON and store as Map to write as CSV later
    ArrayList<Map<String,Object>> listOfDataToWrite = new ArrayList<Map<String,Object>>()

    ArrayList<String> rowKeyList = new ArrayList<String>()
    def validationRow = viewAsJson.rows.get(0)
    //Compare one row object to config first
    validationRow.each { k, v ->
        if (wantedFields.contains(k)) {
            wantedFields.remove(k)
            rowKeyList.add(k)
        }
    }

    ArrayList<String> ticketKeyList = new ArrayList<String>()
    def validationTicket = viewAsJson.rows.ticket.get(0)

    //Compare one ticket object to config first
    validationTicket.each { k, v ->
        if (wantedFields.contains(k)) {
            wantedFields.remove(k)
            ticketKeyList.add(k)
        }
    }

    def rows = viewAsJson.rows
    def tickets = viewAsJson.rows.ticket

    //Pull matching ticket objects from JSON and store in Map

    ArrayList<Map<String,Object>> tickList= new ArrayList<>()
    ArrayList<Map<String,Object>> rowList= new ArrayList<>()
    rows.each { row ->
        Map<String,Object> rowMap = new HashMap<>()
        row.each { k, v ->
            if(rowKeyList.contains(k))
                rowMap.put(k,v)
        }
        rowList.add(rowMap)
    }

    tickets.each { ticket ->
        Map<String,Object> ticketMap = new HashMap<>()
        ticket.each { k, v ->
            if(ticketKeyList.contains(k))
                ticketMap.put(k, v)
        }
        tickList.add(ticketMap)
    }

    for (int i = 0; i < rowList.size(); i++) {
        HashMap<String,Object> dataMap = new HashMap<>()
        dataMap.putAll(rowList.get(i))
        dataMap.putAll(tickList.get(i))
        listOfDataToWrite.add(dataMap)
    }

    println listOfDataToWrite
    return listOfDataToWrite
}

I know there should be some validation for if the wantedFields ArrayList is still populated. I've iterated on this code so many times I just forgot to re-add that this time.


Solution

  • I don't know if you still need this code but why not try something like this. Have a translation map and run each row through it.

    Object tranverseMapForValue(Map source, String keysToTranverse, Integer location = 0){
        List keysToTranverseList = keysToTranverse.split(/\./)
        tranverseMapForValue(source, keysToTranverseList, location)
    }
    Object tranverseMapForValue(Map source, List keysToTranverse, Integer location = 0){
        if(source.isEmpty() || keysToTranverse.isEmpty()){
            return null
        }
    
        String key = keysToTranverse[location]
        if(source[key] instanceof Map){
            return tranverseMapForValue(source[key], keysToTranverse, location + 1)
        }
        else{
            return source[key]
        }
    }
    
    
    Map translation = [
        "ticket.id": "id",
        "ticket.subject": "subject",
        "requester_id": "requester_id",
        "ticket.status": "status",
        "priority": "priority",
        "updated": "updated",
        "ticket.url": "url"
    ]
    
    List rows = []
    
    json.rows.each{ row ->
        Map mapForRow = [:]
    
        translation.each{ sourceKey, newKey ->
            mapForRow << [(newKey): tranverseMapForValue(row, sourceKey)]
        }
    
        rows.add(mapForRow)
    }