Below is my attempt to learn and apply functional programming using json mapping as example. Will see how far I can go and accomplish when using functional programming, compare to procedural programming that I familiar with.

Transform Flat Json to Nested Json using JsonSlurper, groupBy, collect, sort and JsonBuilder

This example do mapping from source json to target json, group by OrderID, ProductID, and sorted descending by OrderID. When JsonSlurper read json payload, sourceJson is Groovy Map. then construct targetJson nested Map, and finally pass nested Map to JsonBuilder to output final target json payload.

def sourceFile = new File('C:\\Data\\orders_flat.json')
def sourceJson = new JsonSlurper().parseText(sourceFile.getText('UTF-8'))

def targetJson = [
    "Orders" : sourceJson.groupBy{ x -> x.OrderID }.sort{ a, b -> b.key <=> a.key }.collect{ o_grp ->
        [
            "OrderID" : o_grp.key,
            "Customer" : o_grp.value.Customer[0],
            "Country" : o_grp.value.Country[0],
            "Items" : o_grp.value.groupBy{ x -> x.ProductID }.collect{ p_grp ->
                [
                    "ProductID" : p_grp.key,
                    "UnitPrice" : p_grp.value.UnitPrice[0],
                    "Quantity" : p_grp.value.Quantity.sum(),
                    "Variants" : p_grp.value.collect{ v ->
                        [
                            "Color" : v.Color,
                            "Quantity" : v.Quantity
                        ]
                    }
                ]
            }
        ]
    }
]

def builder = new JsonBuilder(targetJson)

print JsonOutput.prettyPrint(builder.toString())

Source Json:

[
  {
    "OrderID": 2501,
    "Customer": "Alex",
    "Country": "USA",
    "ProductID": "P05",
    "UnitPrice": 50,
    "Color": "Blue",
    "Quantity": 1
  },
  {
    "OrderID": 2502,
    "Customer": "Bob",
    "Country": "UK",
    "ProductID": "P05",
    "UnitPrice": 50,
    "Color": "Blue",
    "Quantity": 2
  },
  {
    "OrderID": 2502,
    "Customer": "Bob",
    "Country": "UK",
    "ProductID": "P05",
    "UnitPrice": 50,
    "Color": "Red",
    "Quantity": 3
  },
  {
    "OrderID": 2502,
    "Customer": "Bob",
    "Country": "UK",
    "ProductID": "P06",
    "UnitPrice": 80,
    "Color": "Yellow",
    "Quantity": 1
  },
  {
    "OrderID": 2503,
    "Customer": "Peter",
    "Country": "UK",
    "ProductID": "P07",
    "UnitPrice": 120,
    "Color": "Green",
    "Quantity": 4
  },
  {
    "OrderID": 2503,
    "Customer": "Peter",
    "Country": "UK",
    "ProductID": "P07",
    "UnitPrice": 120,
    "Color": "Yellow",
    "Quantity": 6
  }
]

Target Json:

{
    "Orders": [{
            "OrderID": 2503,
            "Customer": "Peter",
            "Country": "UK",
            "Items": [{
                    "ProductID": "P07",
                    "UnitPrice": 120,
                    "Quantity": 10,
                    "Variants": [{
                            "Color": "Green",
                            "Quantity": 4
                        }, {
                            "Color": "Yellow",
                            "Quantity": 6
                        }
                    ]
                }
            ]
        }, {
            "OrderID": 2502,
            "Customer": "Bob",
            "Country": "UK",
            "Items": [{
                    "ProductID": "P05",
                    "UnitPrice": 50,
                    "Quantity": 5,
                    "Variants": [{
                            "Color": "Blue",
                            "Quantity": 2
                        }, {
                            "Color": "Red",
                            "Quantity": 3
                        }
                    ]
                }, {
                    "ProductID": "P06",
                    "UnitPrice": 80,
                    "Quantity": 1,
                    "Variants": [{
                            "Color": "Yellow",
                            "Quantity": 1
                        }
                    ]
                }
            ]
        }, {
            "OrderID": 2501,
            "Customer": "Alex",
            "Country": "USA",
            "Items": [{
                    "ProductID": "P05",
                    "UnitPrice": 50,
                    "Quantity": 1,
                    "Variants": [{
                            "Color": "Blue",
                            "Quantity": 1
                        }
                    ]
                }
            ]
        }
    ]
}

Extract summary and total using inject

This example show usage of inject function, it is similar to fold/reduce in other functional programming language. Basically it accept collection, accumulate all result from right side of function and return as final result.

def sourceFile = new File('C:\\Data\\orders_flat.json')
def sourceJson = new JsonSlurper().parseText(sourceFile.getText('UTF-8'))

def summaryJson = [
        "Summary":[
                "OrderIDList" : sourceJson.inject([]){ list, current ->
                    list << current.OrderID
                }.unique(),
                "CustomerList" : sourceJson.inject([]){ list, current ->
                    list << current.Customer
                }.unique(),
                "CountryList" : sourceJson.inject([]){ list, current ->
                    list << current.Country
                }.unique(),
                "ProductIDList" : sourceJson.inject([]){ list, current ->
                    list << current.ProductID
                }.unique(),
                "ColorList" : sourceJson.inject([]){ list, current ->
                    list << current.Color
                }.unique(),
                "RowCount" : sourceJson.size(),
                "TotalQuantity" : sourceJson.inject(0){ sum, current ->
                    sum = sum + current.Quantity
                },
                "TotalPrice" : sourceJson.inject(0){ sum, current ->
                    sum = sum + current.Quantity * current.UnitPrice
                }
        ]
]

def builder = new JsonBuilder(summaryJson)

print JsonOutput.prettyPrint(builder.toString())

Source Json:

[
  {
    "OrderID": 2501,
    "Customer": "Alex",
    "Country": "USA",
    "ProductID": "P05",
    "UnitPrice": 50,
    "Color": "Blue",
    "Quantity": 1
  },
  {
    "OrderID": 2502,
    "Customer": "Bob",
    "Country": "UK",
    "ProductID": "P05",
    "UnitPrice": 50,
    "Color": "Blue",
    "Quantity": 2
  },
  {
    "OrderID": 2502,
    "Customer": "Bob",
    "Country": "UK",
    "ProductID": "P05",
    "UnitPrice": 50,
    "Color": "Red",
    "Quantity": 3
  },
  {
    "OrderID": 2502,
    "Customer": "Bob",
    "Country": "UK",
    "ProductID": "P06",
    "UnitPrice": 80,
    "Color": "Yellow",
    "Quantity": 1
  },
  {
    "OrderID": 2503,
    "Customer": "Peter",
    "Country": "UK",
    "ProductID": "P07",
    "UnitPrice": 120,
    "Color": "Green",
    "Quantity": 4
  },
  {
    "OrderID": 2503,
    "Customer": "Peter",
    "Country": "UK",
    "ProductID": "P07",
    "UnitPrice": 120,
    "Color": "Yellow",
    "Quantity": 6
  }
]

Target Json:

{
    "Summary": {
        "OrderIDList": [
            2501,
            2502,
            2503
        ],
        "CustomerList": [
            "Alex",
            "Bob",
            "Peter"
        ],
        "CountryList": [
            "USA",
            "UK"
        ],
        "ProductIDList": [
            "P05",
            "P06",
            "P07"
        ],
        "ColorList": [
            "Blue",
            "Red",
            "Yellow",
            "Green"
        ],
        "RowCount": 6,
        "TotalQuantity": 17,
        "TotalPrice": 1580
    }
}

Filter Json array by conditions using findAll and curry

Filter by condition with example below:

def sourceFile = new File('C:\\Data\\orders_flat.json')
def sourceJson = new JsonSlurper().parseText(sourceFile.getText('UTF-8'))

def FilterByCondition = { String product, String color, def x ->
    x.ProductID == product && x.Color == color
}

def P05 = FilterByCondition.curry("P05")
def P05_and_Red = P05.curry("Red")
def P05_and_Blue = P05.curry("Blue")
def P07_and_Yellow = FilterByCondition.curry("P07").curry("Yellow")

def builder
def filterJson

println "ProductID is P05 and Color is Red :"
filterJson = sourceJson.findAll(){ x -> P05_and_Red(x) }
builder = new JsonBuilder(filterJson)
println builder.toPrettyString()
println ""

println "ProductID is P05 and Color is Blue :"
filterJson = sourceJson.findAll(){ x -> P05_and_Blue(x) }
builder = new JsonBuilder(filterJson)
println builder.toPrettyString()
println ""

println "ProductID is P07 and Color is Yellow :"
filterJson = sourceJson.findAll(){ x -> P07_and_Yellow(x) }
builder = new JsonBuilder(filterJson)
println builder.toPrettyString()
println ""

println "ProductID is P07 and Color is Green :"
filterJson = sourceJson.findAll(){ x -> x.ProductID == "P07" && x.Color == "Green" }
builder = new JsonBuilder(filterJson)
println builder.toPrettyString()
println ""

Source Json:

[
  {
    "OrderID": 2501,
    "Customer": "Alex",
    "Country": "USA",
    "ProductID": "P05",
    "UnitPrice": 50,
    "Color": "Blue",
    "Quantity": 1
  },
  {
    "OrderID": 2502,
    "Customer": "Bob",
    "Country": "UK",
    "ProductID": "P05",
    "UnitPrice": 50,
    "Color": "Blue",
    "Quantity": 2
  },
  {
    "OrderID": 2502,
    "Customer": "Bob",
    "Country": "UK",
    "ProductID": "P05",
    "UnitPrice": 50,
    "Color": "Red",
    "Quantity": 3
  },
  {
    "OrderID": 2502,
    "Customer": "Bob",
    "Country": "UK",
    "ProductID": "P06",
    "UnitPrice": 80,
    "Color": "Yellow",
    "Quantity": 1
  },
  {
    "OrderID": 2503,
    "Customer": "Peter",
    "Country": "UK",
    "ProductID": "P07",
    "UnitPrice": 120,
    "Color": "Green",
    "Quantity": 4
  },
  {
    "OrderID": 2503,
    "Customer": "Peter",
    "Country": "UK",
    "ProductID": "P07",
    "UnitPrice": 120,
    "Color": "Yellow",
    "Quantity": 6
  }
]

Target Json

ProductID is P05 and Color is Red :
[
    {
        "OrderID": 2502,
        "Customer": "Bob",
        "Country": "UK",
        "ProductID": "P05",
        "UnitPrice": 50,
        "Color": "Red",
        "Quantity": 3
    }
]

ProductID is P05 and Color is Blue :
[
    {
        "OrderID": 2501,
        "Customer": "Alex",
        "Country": "USA",
        "ProductID": "P05",
        "UnitPrice": 50,
        "Color": "Blue",
        "Quantity": 1
    },
    {
        "OrderID": 2502,
        "Customer": "Bob",
        "Country": "UK",
        "ProductID": "P05",
        "UnitPrice": 50,
        "Color": "Blue",
        "Quantity": 2
    }
]

ProductID is P07 and Color is Yellow :
[
    {
        "OrderID": 2503,
        "Customer": "Peter",
        "Country": "UK",
        "ProductID": "P07",
        "UnitPrice": 120,
        "Color": "Yellow",
        "Quantity": 6
    }
]

ProductID is P07 and Color is Green :
[
    {
        "OrderID": 2503,
        "Customer": "Peter",
        "Country": "UK",
        "ProductID": "P07",
        "UnitPrice": 120,
        "Color": "Green",
        "Quantity": 4
    }
]

Recursive traverse nested Json and apply transformation based on condition

goDeep function will recursively traverse all json node, and will apply transformation using higher order function(HOF) passed via parameter doSomething. The transformation is apply on the value of json, not on the key.

Since goDeep recursive traversal is same for different transformation, so to reuse goDeep, pass below list of HOF to goDeep function to have different transformation:
toUpperCase will convert string value to upper case.
nullToEmpty will convert all null value become empty string.
trimLeftRight will trim string value.
leftPadZeroIfKeyMatched will left pad zero on value based on matching key and number of characters set.
leftPadZeroIfKeyId using curry to left pad zero with total 5 chars when key = id
leftPadZeroIfKeyQty using curry to left pad zero with total 8 chars when key = qty

Using above convention, is flexible to build other functions that do different transformation, while still reuse goDeep recursive traversal. Recursive traversal is especially useful if the source json payload’s structure is not fixed, unknown in advance, or is impossible or too cumbersome to identify all required fields that need transformation, so let recursive do it work, apply necessary transformation when condition is met regardless of how deep the json structure.

import groovy.json.*

def goDeep(def root, def doSomething) {
    if (root instanceof List) {
        root.collect {
            if (it instanceof Map) {
                goDeep(it, doSomething)
            }
            else if (it instanceof List) {
                goDeep(it, doSomething)
            }
            else {
                it = doSomething(null, it)
            }
        }
    } else if (root instanceof Map) {
        root.each {
            if (it.value instanceof Map) {
                goDeep(it.value, doSomething)
            }
            else if (it.value instanceof List) {
                it.value = goDeep(it.value, doSomething)
            }
            else{
                it.value = doSomething(it.key, it.value)
            }
        }
    }
}

def toUpperCase = { key, value ->
    if(value != null) {
        switch (value.class.getSimpleName()) {
            case "String":
                value = value.toUpperCase()
                break;
            default:
                break;
        }
    }
    return value
}

def leftPadZeroIfKeyMatched = {matchingKey, numOfChar, key, value ->
    if(value != null) {
        if (key == matchingKey) {
            switch (value.class.getSimpleName()) {
                case "String":
                    value = value.toString().padLeft(numOfChar, "0")
                    break;
                case "Integer":
                    value = value.toString().padLeft(numOfChar, "0")
                    break;
                default:
                    break;
            }
        }
    }
    return value
}

def trimLeftRight = {key, value ->
    if(value != null) {
        switch (value.class.getSimpleName()) {
            case "String":
                value = value.toString().trim()
                break;
            default:
                break;
        }
    }
    return value
}

def nullToEmpty = {key, value ->
    if(value == null) {
        value = ""
    }
    return value
}

def leftPadZeroIfKeyId = leftPadZeroIfKeyMatched.curry("id").curry(5)
def leftPadZeroIfKeyQty = leftPadZeroIfKeyMatched.curry("qty").curry(8)

def sourceFile = new File('C:\\Data\\sourceJson.json')
def sourceJson = new JsonSlurper().parseText(sourceFile.getText('UTF-8'))

def targetJson = goDeep(sourceJson, trimLeftRight)
targetJson = goDeep(targetJson, nullToEmpty)
targetJson = goDeep(targetJson, leftPadZeroIfKeyId)
targetJson = goDeep(targetJson, leftPadZeroIfKeyQty)
targetJson = goDeep(targetJson, toUpperCase)

def builder = new JsonBuilder(targetJson)
println builder.toPrettyString()

Source Json:

{
    "header": {
        "id": 222,
		"customer": "John",
		"qty": 60,
		"note": null
    },
    "items": [{
            "id": 580,
			"fruit": "Apple  ",
			"qty": 30,
			"comment": null,
            "lines": [{
                    "id": 8001,
					"size": "big",
					"qty": 10
                }, {
                    "id": 8002,
					"size": "small",
					"qty": 20
                }
            ],
        }, {
            "id": 581,
			"fruit": "     Orange ",
			"comment": null,
            "lines": [{
                    "id": 8003,
					"size": "medium",
					"qty": 30
                }

            ]
        }
    ]
}

Target Json:

{
    "header": {
        "id": "00222",
        "customer": "JOHN",
        "qty": "00000060",
        "note": ""
    },
    "items": [{
            "id": "00580",
            "fruit": "APPLE",
            "qty": "00000030",
            "comment": "",
            "lines": [{
                    "id": "08001",
                    "size": "BIG",
                    "qty": "00000010"
                }, {
                    "id": "08002",
                    "size": "SMALL",
                    "qty": "00000020"
                }
            ]
        }, {
            "id": "00581",
            "fruit": "ORANGE",
            "comment": "",
            "lines": [{
                    "id": "08003",
                    "size": "MEDIUM",
                    "qty": "00000030"
                }
            ]
        }
    ]
}
Groovy – Json Mapping with Functional Programming Example
Tagged on:                                 

Leave a Reply

Your email address will not be published.