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
Newsletter
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Testing Java and Spring Applications With Kotlin: Low-Risk to Adoption
  • Java Module Benefits With Example
  • Java and Low Latency
  • Why Kotlin or Java Is the Wrong Question For Enterprises

Trending

  • SSRF Prevention in C#: Securing File Imports From User-Supplied URLs
  • The Three Tiers of Agentic Incident Response: When to Trust AI Autonomy
  • Beyond the Endpoint: How Agent Cards Enable Discovery, Interoperability, and Trust
  • Giving AI Agents Safe Access to Legacy Systems: Introducing legacy2mcp

Draft This article hasn't been published, so readers can't see it.

In moderation This article is waiting for review, so readers can't see it yet.

Deleted This article has been deleted, so readers can't see it.

  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
·
Nov. 04, 20 · Tutorial
Post on X (opens in a new tab) Share on LinkedIn (opens in a new tab)
11.5K Views

Join the DZone community Save articles, join the discussion, and download Refcards and Trend Reports with a free account.

Join for free Already a member? Log in

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

  • Testing Java and Spring Applications With Kotlin: Low-Risk to Adoption
  • Java Module Benefits With Example
  • Java and Low Latency
  • Why Kotlin or Java Is the Wrong Question For Enterprises

Partner Resources

×

Comments

Loading comments

The comments didn't load

Please try again in a moment.

Likes

Loading

The next likes didn't load. Please try again.

The likes didn't load

Please try again in a moment.

See who liked this

Log in or join DZone for free to see who liked this article.

Log in Join for free
  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

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 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook