Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Exploring Kotlin's DSL

DZone's Guide to

Exploring Kotlin's DSL

Here we see how to use Kotlin's DSL to represent domain operations. We use JSON in this example, expressing it in Kotlin and outputting the result to JSON.

· Java Zone
Free Resource

Try Okta to add social login, MFA, and OpenID Connect support to your Java app in minutes. Create a free developer account today and never build auth again.

Kotlin is a statically typed JVM language that provides excellent support for a DSL. Since that DSL blueprint is well-embedded within Kotlin, we can express a domain-specific operation much more concisely than an equivalent piece of code in a general-purpose language.

Let's try and understand this with an example. Here, what we want to do is create a DSL to express JSON in Kotlin and output the result to actual JSON. So, we should be able to represent the following JSON in our code:

{
    "language": "Kotlin",
    "description" : "Statically typed JVM language"
    "version" : "1.1.4"
}


In order to express the same in Kotlin's DSL, the first thing that we may need to consider is how should we model this in code. Since we are trying to represent JSON, we could think of representing it with a bunch of classes, including Json and Obj.

class Json {
    private lateinit var jsonObject: Obj
}

class Obj {
    private val entries = linkedMapOf<String, Any>() //representing key/value pair
}


For simplicity, we shall assume that we can have a single object within the Json class. With this done, we can think of creating a DSL to represent JSON.

We could think of representing JSON in the form of the following DSL:

json {
    obj {
        "language" to "kotlin"
        "description" to "Statically typed JVM language"
        "version" to "1.1.4"
    }
}


Here, json, obj, and to are functions, of which to appears to be the simplest. to is a simple extension function, which is defined via the String class.

fun String.to(value: Any): Unit {
    //code ommitted
}


The next point to be understood is where the to function should be defined. Looking at the above DSL, the to function is invoked within the context of obj, meaning an instance of Obj should be available to invoke the to function. Let's define the obj function:

infix fun obj(init: Obj.() -> Unit): Obj {
    val obj    = Obj()
    obj.init()
    jsonObject = obj

    return obj
}


This function takes one parameter named init, which is also a function. The type of the function is Obj.() -> Unit, which is a function type with a receiver. This means that this function can be invoked on an instance of Obj (receiver) and we can call/access members of that instance inside the function.

With the above reasoning, we can define a json function as:

fun json(init: Json.() -> Unit): Json {
    val json = Json()
    json.init()
    return json
}


So, what does this function do? It creates a new instance of Json, then initializes it by calling the function that is passed as an argument (in our example, this boils down to calling obj on the Json instance), and then it returns the newly created Json instance. 

Now, with the definition of json and obj, we can write the complete code our problem statement:

fun json(init: Json.() -> Unit): Json {
    val json = Json()
    json.init()
    return json
}

class Json {
    private lateinit var jsonObject: Obj

  infix fun obj(init: Obj.() -> Unit): Obj {
        val obj    = Obj()
        obj.init()
        jsonObject = obj

        return obj
    }
    fun render(): String = jsonObject.toJsonString()
}

class Obj {
    private val entries = linkedMapOf<String, Any>()

    infix fun String.to(value: Any): Unit {
        entries.put(this, value)
    }
  fun toJsonString(indent: String = " "): String {
      val sb = StringBuilder().append("{").append("\n")
        for ( (k, v) in entries ){
          with(sb) {
            append("""$indent"$k" : """)
            when(v){
              is String    -> append(""""$v"""")
              is Array<*>  -> append("[${v.joinToString()}]")
              else         -> append("$v")                                   
            }
            append("\n")
          }
      }
      return sb.append("$indent}").toString()
    }
}


Using the code behind the DSL is pretty straightforward:

val expectedJson = """
    {
                       "language" : "Kotlin"
                       "description" : "Statically typed JVM language"
                       "version" : "1.1.4"
                    }
   """
val json = json {
              obj {
                "language" to "Kotlin"
                "description" to "Statically typed JVM language"
                "version" to "1.1.4"
              }
          }

json.render() shouldBe expectedJson


This is a basic implementation of a DSL in Kotlin to represent JSON. It looks concise and expressive enough to represent domain operations.

Implementation with support for nested JSON structures can be found here.

Build and launch faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:
java ,kotlin dsl ,json ,domain operations ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}