Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin DSL for creating json objects (without creating garbage)

Tags:

kotlin

I am trying to create a DSL for creating JSONObjects. Here is a builder class and a sample usage:

import org.json.JSONObject

fun json(build: JsonObjectBuilder.() -> Unit): JSONObject {
    val builder = JsonObjectBuilder()
    builder.build()
    return builder.json
}

class JsonObjectBuilder {
    val json = JSONObject()

    infix fun <T> String.To(value: T) {
        json.put(this, value)
    }
}

fun main(args: Array<String>) {
    val jsonObject =
            json {
                "name" To "ilkin"
                "age" To 37
                "male" To true
                "contact" To json {
                    "city" To "istanbul"
                    "email" To "[email protected]"
                }
            }
    println(jsonObject)
}

The output of the above code is :

{"contact":{"city":"istanbul","email":"[email protected]"},"name":"ilkin","age":37,"male":true}

It works as expected. But it creates an additional JsonObjectBuilder instance every time it creates a json object. Is it possible to write a DSL for creating json objects without extra garbage?

like image 915
ilkinulas Avatar asked Jan 25 '17 20:01

ilkinulas


4 Answers

You can use a Deque as a stack to track your current JSONObject context with a single JsonObjectBuilder:

fun json(build: JsonObjectBuilder.() -> Unit): JSONObject {
    return JsonObjectBuilder().json(build)
}

class JsonObjectBuilder {
    private val deque: Deque<JSONObject> = ArrayDeque()

    fun json(build: JsonObjectBuilder.() -> Unit): JSONObject {
        deque.push(JSONObject())
        this.build()
        return deque.pop()
    }

    infix fun <T> String.To(value: T) {
        deque.peek().put(this, value)
    }
}

fun main(args: Array<String>) {
    val jsonObject =
            json {
                "name" To "ilkin"
                "age" To 37
                "male" To true
                "contact" To json {
                    "city" To "istanbul"
                    "email" To "[email protected]"
                }
            }
    println(jsonObject)
}

Example output:

{"contact":{"city":"istanbul","email":"[email protected]"},"name":"ilkin","age":37,"male":true}

Calling json and build across multiple threads on a single JsonObjectBuilder would be problematic but that shouldn't be a problem for your use case.

like image 193
mfulton26 Avatar answered Oct 19 '22 16:10

mfulton26


Do you need a DSL? You lose the ability to enforce String keys, but vanilla Kotlin isn't that bad :)

JSONObject(mapOf(
        "name" to "ilkin",
        "age" to 37,
        "male" to true,
        "contact" to mapOf(
                "city" to "istanbul",
                "email" to "[email protected]"
        )
))
like image 30
James Bassett Avatar answered Oct 19 '22 15:10

James Bassett


I am not sure if I get the question correctly. You don't want a builder?

import org.json.JSONArray
import org.json.JSONObject

class Json() {

    private val json = JSONObject()

    constructor(init: Json.() -> Unit) : this() {
        this.init()
    }

    infix fun String.to(value: Json) {
        json.put(this, value.json)
    }

    infix fun <T> String.to(value: T) {
        json.put(this, value)
    }

    override fun toString(): String {
        return json.toString()
    }
}

fun main(args: Array<String>) {

    val json = Json {
        "name" to "Roy"
        "body" to Json {
            "height" to 173
            "weight" to 80
        }
        "cars" to JSONArray().apply {
            put("Tesla")
            put("Porsche")
            put("BMW")
            put("Ferrari")
        }
    }

    println(json)

}

You will get

{
  "name": "Roy",
  "body": {
    "weight": 80,
    "height": 173
  },
  "cars": [
    "Tesla",
    "Porsche",
    "BMW",
    "Ferrari"
  ]
}
like image 7
Arst Avatar answered Oct 19 '22 16:10

Arst


Yes, it is possible if you don't need any intermediate representation of the nodes, and if the context is always the same (the recursive calls are no different from each other). This can be done by writing the output immediately.

However, this severely increases code complexity, because you have to process your DSL calls right away without storing them anywhere (again, to avoid redundant objects).

Example (see its demo here):

class JsonContext internal constructor() {
    internal val output = StringBuilder()

    private var indentation = 4

    private fun StringBuilder.indent() = apply {
        for (i in 1..indentation)
            append(' ')
    }

    private var needsSeparator = false

    private fun StringBuilder.separator() = apply { 
        if (needsSeparator) append(",\n")
    }

    infix fun String.to(value: Any) {
        output.separator().indent().append("\"$this\": \"$value\"")
        needsSeparator = true
    }

    infix fun String.toJson(block: JsonContext.() -> Unit) {
        output.separator().indent().append("\"$this\": {\n")
        indentation += 4
        needsSeparator = false
        block(this@JsonContext)
        needsSeparator = true
        indentation -= 4
        output.append("\n").indent().append("}")
    }
}

fun json(block: JsonContext.() -> Unit) = JsonContext().run {
    block()
    "{\n" + output.toString() + "\n}"
}

val j = json {
    "a" to 1
    "b" to "abc"
    "c" toJson {
        "d" to 123
        "e" toJson {
            "f" to "g"
        }
    }
}

If you don't need indentation but only valid JSON, this can be easily simplified, though.

You can make the json { } and .toJson { } functions inline to get rid even of the lambda classes and thus you achieve almost zero object overhead (one JsonContext and the StringBuilder with its buffers are still allocated), but that would require you to change the visibility modifiers of the members these functions use: public inline functions can only access public or @PublishedApi internal members.

like image 2
hotkey Avatar answered Oct 19 '22 17:10

hotkey