DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Java Module Benefits With Example
  • Java and Low Latency
  • Java Virtual Threads and Scaling
  • Java’s Next Act: Native Speed for a Cloud-Native World

Trending

  • Implementing Explainable AI in CRM Using Stream Processing
  • Optimizing Serverless Computing with AWS Lambda Layers and CloudFormation
  • When Airflow Tasks Get Stuck in Queued: A Real-World Debugging Story
  • How to Introduce a New API Quickly Using Micronaut
  1. DZone
  2. Coding
  3. Java
  4. Introducing MapNeat, a JVM JSON Transformation Library

Introducing MapNeat, a JVM JSON Transformation Library

Given Kotlin's high interoperability with most of the JVM languages, MapNeat is easy to use in any Java project without any particular hassle.

By 
Andrei Ciobanu user avatar
Andrei Ciobanu
·
Nov. 04, 20 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
10.9K Views

Join the DZone community and get the full member experience.

Join For Free

MapNeat is a JVM library written in Kotlin, that provides an easy to use DSL (Domain Specific Language) for manipulating and transforming existing JSONs, XMLs, or POJOs into new JSONs. The trick is that no intermediary model classes are needed, and all the changes are done using a mostly descriptive approach.

The library can be particularly useful when integrating various systems that need to exchange messages in different formats, creating DTOs, etc.

Given Kotlin's high interoperability with most of the JVM languages, MapNeat is easy to use in any Java project without any particular hassle.

How it Works

A typical transformation starts with the source input (JSON, XML, or any Java Object), and then it contains a series of Operations applied in order:

Kotlin
 




x


 
1
val jsonValue : String = "..."
2
val transformedJson = json(fromJson(jsonValue)) {
3
    /* operation1 */
4
    /* operation2 */
5
    /* operation3 */
6
    /* conditional block */
7
        /* operation4 */
8
}.getPrettyString() // Transformed output



If the source is XML, "fromXML(xmlValue: String)" can be used. In this case, the "xmlValue" is automatically converted to JSON using JSON In Java.

If the source is a POJO, "fromObject(object)" can be used. In this case, the object is automatically converted to JSON using Jackson.

The First Example

As a rule, multiple inputs (sources) can be used inside a transformation. So, given two JSON objects JSON1 and JSON2:

JSON1

JSON
 




xxxxxxxxxx
1
25


 
1
{
2
  "id": 380557,
3
  "first_name": "Gary",
4
  "last_name": "Young",
5
  "photo": "http://srcimg.com/100/150",
6
  "married": false,
7
  "visits" : [ 
8
    {
9
        "country" : "Romania",
10
        "date" : "2020-10-10"
11
    },
12
    {
13
        "country" : "Romania",
14
        "date" : "2019-07-21"
15
    },
16
    {
17
        "country" : "Italy",
18
        "date" : "2019-12-21"
19
    },
20
    {
21
        "country" : "France",
22
        "date" : "2019-02-21"
23
    }
24
  ]
25
}



JSON2

JSON
 




xxxxxxxxxx
1


 
1
{
2
  "citizenship" : [ "Romanian", "French" ]
3
}



We write the transformation as:

Kotlin
 




xxxxxxxxxx
1
51


 
1
val transform = json(fromJson(JSON1)) {
2

          
3
        "person.id"         /= 100
4
        "person.firstName"  *= "$.first_name"
5
        "person.lastName"   *= "$.last_name"
6

          
7
        // We can using a nested json assignment instead of using the "." notation
8
        "person.meta" /= json {
9
            "information1" /= "ABC"
10
            "information2" /= "ABC2"
11
        }
12
        
13
        // We can assign a value from a lambda expression
14
        "person.maritalStatus" /= {
15
            if(sourceCtx().read("$.married")) 
16
                "married" 
17
            else 
18
                "unmarried"
19
        }
20

          
21
        "person.visited" *= {
22
            // We select only the country name from the visits array
23
            expression = "$.visits[*].country"
24
            processor = { countries ->
25
                // We don't allow duplications so we create a Set
26
                (countries as List<String>).toMutableSet()
27
            }
28
        }
29

          
30
        // We add a new country using the "[+]" notation
31
        "person.visited[+]" /= "Ireland"
32

          
33
        // We merge another array into the visited[] array
34
        "person.visited[++]" /= mutableListOf("Israel", "Japan")
35

          
36
        // We look into a secondary json source - JSON2
37
        // Assigning the citizenship array to a temporary path (person._tmp.array)
38
        "person._tmp" /= json(fromJson(JSON2)) {
39
            "array" *= "$.citizenship"
40
        }
41

          
42
        // We copy the content of temporary array into the path were we want to keep it
43
        "person._tmp.array" % "person.citizenships"
44

          
45
        // We remove the temporary path
46
        - "person._tmp"
47

          
48
        // We rename "citizenships" to "citizenship" because we don't like typos
49
        "person.citizenships" %= "person.citizenship"
50
}
51

          



After all the operations are performed step by step, the output looks like this:

JSON
 




xxxxxxxxxx
1
14


 
1
{
2
  "person" : {
3
    "id" : 100,
4
    "firstName" : "Gary",
5
    "lastName" : "Young",
6
    "meta" : {
7
      "information1" : "ABC",
8
      "information2" : "ABC2"
9
    },
10
    "maritalStatus" : "unmarried",
11
    "visited" : [ "Romania", "Italy", "France", "Ireland", "Israel", "Japan" ],
12
    "citizenship" : [ "Romanian", "French" ]
13
  }
14
}



Operations

In the previous example, you might wonder what the operators /=, *=, %, %=, -  are doing.

Those are actually shortcuts methods for the operations we are performing:

Operator Operation Description
/= assign Assigns a given constant or a value computed in a lambda expression to a certain path in the target JSON (the result).
*= shift Shifts a portion from the source JSON based on a JSON Path expression.
% copy Copies a path from the target JSON to another path.
%= move Moves a path from the target JSON to another path.
- delete Deletes a path from the target JSON.

Additionally, the paths from the target JSON can be "decorated" with "array notation":

Array Notation Description
path[] A new array will be created through the assign and shift operations.
path[+] An append will be performed through the assign and shift operations.
path[++] A merge will be performed through the assign and shift operations.

If you prefer, instead of using the operators you can use their equivalent methods.

For example, assign (/=):

Kotlin
 




x


 
1
"person.name" /= "Andrei"



Can be written as ("assign"):

Kotlin
 




xxxxxxxxxx
1


 
1
"person.name" assign "Andrei"



Or (*=):

Kotlin
 




xxxxxxxxxx
1


 
1
"person.name" *= "$.user.full_name"



Can be written as:

Kotlin
 




xxxxxxxxxx
1


 
1
"person.name" shift "$.user.full_name"



Personally, I prefer the operator notation (/=, *=, etc.), but some people consider the methods (assign, shift) more readable.

For the rest of the examples, the operator notation will be used.

Assign (/=)

The Assign Operation is used to assign a value to a path in the resulting JSON (target).

The value can be a constant object, or a lambda (()-> Any).

Example:

Kotlin
 




xxxxxxxxxx
1
38


 
1
import net.andreinc.mapneat.dsl.json
2

          
3
const val A_SRC_1 = """
4
{
5
    "id": 380557,
6
    "first_name": "Gary",
7
    "last_name": "Young"
8
}    
9
"""
10

          
11
const val A_SRC_2 = """
12
{
13
    "photo": "http://srcimg.com/100/150",
14
    "married": false
15
}
16
"""
17

          
18
fun main() {
19
    val transformed = json(A_SRC_1) {
20
        // Assigning a constant
21
        "user.user_name" /= "neo2020"
22

          
23
        // Assigning value from a lambda expression
24
        "user.first_name" /= { sourceCtx().read("$.first_name") }
25

          
26
        // Assigning value from another JSON source
27
        "more_info" /= json(A_SRC_2) {
28
            "married" /= { sourceCtx().read("$.married") }
29
        }
30

          
31
        // Assigning an inner JSON with the same source as the parent
32
        "more_info2" /= json {
33
            "last_name" /= { sourceCtx().read("$.last_name") }
34
        }
35
    }
36
    
37
    println(transformed)
38
}



The target JSON looks like:

JSON
 




xxxxxxxxxx
1
12


 
1
{
2
  "user" : {
3
    "user_name" : "neo2020",
4
    "first_name" : "Gary"
5
  },
6
  "more_info" : {
7
    "married" : false
8
  },
9
  "more_info2" : {
10
    "last_name" : "Young"
11
  }
12
}



In the lambda method we can access the following methods from the "outer" context:

  • sourceCtx() which represents the ReadContext of the source. We can use this to read JSON Paths just like in the example above;
  • targetCtx() which represents the ReacContext of the target. This is calculated each time we call the method. So, it contains only the changes that were made up until that point. In most cases, this shouldn't be called.

In case we are using an inner JSON structure, we also have reference to the parent source and target contexts:

  • parent.sourceCtx()
  • parent.targetCtx()

parent() returns a Nullable value, so it called with !! (double bang) in Kotlin.

Kotlin
 




x


 
1
... {
2
    "something" /= "Something Value"
3
    "person" /= json {
4
        "innerSomething" /= { parent()!!.targetCtx().read("$.something") }
5
    }
6
}



For more information about ReadContext please check the JSON-path's documentation.

The Assign operation can also be used in conjunction with left-side array notations ([], [+], [++]):

Kotlin
 




xxxxxxxxxx
1
19


 
1
fun main() {
2
    val transformed = json("{}") {
3
        println("Simple array creation:")
4
        "a" /= 1
5
        "b" /= 1
6
        println(this)
7

          
8
        println("Adds a new value in the array:")
9
        "a[+]" /= 2
10
        "b[+]" /= true
11
        println(this)
12

          
13
        println("Merge in an existing array:")
14
        "b[++]" /= arrayOf("a", "b", "c")
15
        println(this)
16
    }
17
}



Output:

Kotlin
 




xxxxxxxxxx
1
15


 
1
Simple array creation:
2
{
3
  "a" : 1,
4
  "b" : 1
5
}
6
Adds a new value in the array
7
{
8
  "a" : [ 1, 2 ],
9
  "b" : [ 1, true ]
10
}
11
Merge in an existing array:
12
{
13
  "a" : [ 1, 2 ],
14
  "b" : [ 1, true, "a", "b", "c" ]
15
}



Shift (*=)

The Shift operation is very similar to the Assign operation, but it provides an easier way to query the source JSON using JSON-path.

Example:

Kotlin
 




xxxxxxxxxx
1
65


 
1
package net.andreinc.mapneat.examples
2

          
3
import net.andreinc.mapneat.dsl.json
4
import java.time.LocalDate
5
import java.time.format.DateTimeFormatter
6

          
7
val JSON_VAL = """
8
{
9
  "id": 380557,
10
  "first_name": "Gary",
11
  "last_name": "Young",
12
  "photo": "http://srcimg.com/100/150",
13
  "married": false,
14
  "visits" : [ 
15
    {
16
        "country" : "Romania",
17
        "date" : "2020-10-10"
18
    },
19
    {
20
        "country" : "Romania",
21
        "date" : "2019-07-21"
22
    },
23
    {
24
        "country" : "Italy",
25
        "date" : "2019-12-21"
26
    },
27
    {
28
        "country" : "France",
29
        "date" : "2019-02-21"
30
    }
31
  ]
32
}
33
"""
34

          
35
fun main() {
36
    val transformed = json(JSON_VAL) {
37
        "user.name.first" *= "$.first_name"
38
        // We use an additional processor to capitalise the last Name
39
        "user.name.last" *= {
40
            expression = "$.last_name"
41
            processor = { (it as String).toUpperCase() }
42
        }
43
        // We add the photo directly into an array
44
        "user.photos[]" *= "$.photo"
45
        // We don't allow duplicates
46
        "user.visits.countries" *= {
47
            expression = "$.visits[*].country"
48
            processor = { (it as MutableList<String>).toSet().toMutableList() }
49
        }
50
        // We keep only the last visit
51
        "user.visits.lastVisit" *= {
52
            expression = "$.visits[*].date"
53
            processor = {
54
                (it as MutableList<String>)
55
                    .stream()
56
                    .map { LocalDate.parse(it, DateTimeFormatter.ISO_DATE) }
57
                    .max(LocalDate::compareTo)
58
                    .get()
59
                    .toString()
60
            }
61
        }
62
    }
63

          
64
    println(transformed)
65
}



Output:

JSON
 




xxxxxxxxxx
1
13


 
1
{
2
  "user" : {
3
    "name" : {
4
      "first" : "Gary",
5
      "last" : "YOUNG"
6
    },
7
    "photos" : [ "http://srcimg.com/100/150" ],
8
    "visits" : {
9
      "countries" : [ "Romania", "Italy", "France" ],
10
      "lastVisit" : "2020-10-10"
11
    }
12
  }
13
}



As you can see in the above example, each expression can be accompanied by an additional processor method that allows developers to refine the results provided by the JSON path expression.

Similar to the Assign lambdas, sourceCtx(), targetCtx(), parent!!.sourceCtx(), parent!!.targetCtx() are also available to the method context and can be used.

Copy (%)

The Copy Operation moves a certain path from the target JSON to another path in the target JSON.

Example:

Kotlin
 




xxxxxxxxxx
1
10


 
1

          
2
import net.andreinc.mapneat.dsl.json
3

          
4
fun main() {
5
    val transformed = json("{}") {
6
        "some.long.path" /= mutableListOf("A, B, C")
7
        "some.long.path" % "copy"
8
        println(this)
9
    }
10
}



Output:

JSON
 




xxxxxxxxxx
1
10
9


 
1
{
2
  "some" : {
3
    "long" : {
4
      "path" : [ "A, B, C" ]
5
    }
6
  },
7
  "copy" : [ "A, B, C" ]
8
}



Move (%=)

The Move operation moves a certain path from the target JSON to a new path in the target JSON.

Example:

Kotlin
 




xxxxxxxxxx
1
11


 
1
package net.andreinc.mapneat.examples
2

          
3
import net.andreinc.mapneat.dsl.json
4

          
5
fun main() {
6
    json("{}") {
7
        "array" /= intArrayOf(1,2,3)
8
        "array" %= "a.b.c.d"
9
        println(this)
10
    }
11
}



Output:

JSON
 




xxxxxxxxxx
1


 
1
{
2
  "a" : {
3
    "b" : {
4
      "c" : {
5
        "d" : [ 1, 2, 3 ]
6
      }
7
    }
8
  }
9
}



Delete (-)

The Delete operation deletes a certain path from the target JSON.

Example:

Kotlin
 




xxxxxxxxxx
1
13


 
1
package net.andreinc.mapneat.examples
2

          
3
import net.andreinc.mapneat.dsl.json
4

          
5
fun main() {
6
    json("{}") {
7
        "a.b.c" /= mutableListOf(1,2,3,4,true)
8
        "a.b.d" /= "a"
9
        // deletes the array from "a.b.c"
10
        - "a.b.c"
11
        println(this)
12
    }
13
}



Output:

JSON
 




xxxxxxxxxx
1


 
1
{
2
  "a" : {
3
    "b" : {
4
      "d" : "a"
5
    }
6
  }
7
}



Using MapNeat From Java

Given Kotlin's high level of interoperability with Java, MapNeat can be used in any Java application.

The DSL file should remain kotlin, but it can be called from any Java program, as simple as:

Kotlin
 




xxxxxxxxxx
1
12


 
1
@file : JvmName("Sample")
2

          
3
package kotlinPrograms
4

          
5
import net.andreinc.mapneat.dsl.json
6

          
7
fun personTransform(input: String) : String {
8
    return json(input) {
9
        "person.name" /= "Andrei"
10
        "person.age" /= 13
11
    }.getPrettyString()
12
}



In the Java file:

Java
 




xxxxxxxxxx
1


 
1
import static kotlinPrograms.Sample.personTransform;
2

          
3
public class Main {
4
    public static void main(String[] args) {
5
        // personTransform(String) is the method from Kotlin
6
        String person = personTransform("{}");
7
        System.out.println(person);
8
    }
9
}



PS: Configuring the Java application to be Kotlin-enabled it's quite simple, usually IntelliJ is doing this automatically without any developer intervention.

file IO Java (programming language) Java virtual machine Kotlin (programming language) Library

Opinions expressed by DZone contributors are their own.

Related

  • Java Module Benefits With Example
  • Java and Low Latency
  • Java Virtual Threads and Scaling
  • Java’s Next Act: Native Speed for a Cloud-Native World

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!