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?
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.
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]"
)
))
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"
]
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With