The Scene
When you write code for distributed apps, especially in the all mighty microservices era, there is one word that keeps showing up during technical conceptions: format.
"What human readable format will our services use to exchange messages?"
"What scheme will we be using to provide the standard output with logs?"
Very often nowadays, the answer lies within four letters: JSON
(JavaScript Object Notation). Although there are other human readable alternatives (yaml
is less verbose, but indentation sensitive; XML
is more verbose etc.) more and more people tend to suggest (and use) JSON
.
Our technical stack, as far as the JVM is concerned, is now completely implemented with Kotlin
+SpringBoot
services. These services produce their logs according to a specific format, that is described using JSON
. They also use JSON
to serialize and deserialize outgoing and incoming messages. These messages are then also validated using JSON schemas.
Long story short, JSON
has become key to how we communicate, manage logs, provide and consume APIs to/from different teams.
The Problem
We all tend to like things simple. However when you are not in a JavaScript
first class citizen technical stack, it can be tedious to always have to write classes just for the sole purpose of serializing and deserializing JSON
. You very often have to rely on a serialization library like Jackson
. To add insult to injury, sometimes when writting unit tests, you just want to provide a method, an endpoint, a BDD test with a simple JSON
object.
What are your choices if you want to avoid relying on a library or a framework?
You could always use Kotlin
's multiline Strings to format your objects, especially since there is a pretty good integration in IntelliJ IDE. You could even inject language reference (Alt+Enter → Inject language or reference → JSON) and would benefit from some coloration/completion while creating your object.
"""{ "cat": "meow", "dog": "woof" }""".trimIndent()
But ultimately, every developer has to remember there is such a functionality in their IDE to benefit from these tools.
However, if you want to play a bit more with formatting and still keep the code maintainance easy for future developers, this is not ideal. You can then decide to switch to a trimMargin()
version.
""" |{ | "cat": "meow", | "dog": "woof" |} """.trimMargin()
But then JSON language injection would be lost because of the introduced |
chars.
These solutions may suit you perfectly. But keeping up with upcoming code change while keeping indentation the way you want can be tricky.
The Idea
It all started from a rather simple observation: Kotlin
internal DSLs allow its users to design APIs that fit well with the language, obviously.
But other interesting aspects arise. Since Kotlin allows for a very good interop with Java
, it is also possible to wrap existing libraries into a nicer looking Kotlin
hierarchical syntax, or even use the language as core descriptor for a build tool, such as Gradle
's Kotlin DSL.
The advantages are numerous: you’ll get code completion & formatting for free while benefiting from the compiler’s type checks.
Without digging too deep into Kotlin
's design, DSLs heavily rely on these 5 language specificities :
Koson was created using these functionalities only. No other third party libraries are used, resulting in a very lightweight Kotlin library.
Koson
Koson was open sourced under the MIT licence in 2018. Some functionalities have been added over the years, but mainly, this is how you use it:
obj
and arr
are the main entry points of the library, allowing to start describing your JSON
content.
Here is another example:
val obj = obj { "key" to 3.4 "anotherKey" to arr["test", "test2", 1, 2.433, true] "nullValue" to null "emptyObject" to obj { } "emptyArray" to arr "custom" to ZonedDateTime.now() } println(obj) <1> println(obj.pretty()) <2>
<1>
Will output a single line formatted JSON
String:
{"key":3.4,"anotherKey":["test","test2",1,2.433,true],"nullValue":null,"emptyObject":{},"emptyArray":[],"custom":"2020-07-15T10:59:19.042965+02:00[Europe/Paris]"}
<2>
Will output a 2 whitespaces indented JSON
String:
{ "key": 3.4, "anotherKey": [ "test", "test2", 1, 2.433, true ], "nullValue": null, "emptyObject": { }, "emptyArray": [ ], "custom": "2020-07-15T10:59:19.042965+02:00[Europe/Paris]" }
Now the good thing is you will benefit from syntax highlighting, code completion, formatting and even type safety.
For instance the library prevents errors at compile time:
Koson also allows the user to feed an Iterable as a source for an array, external JSON source embedding and custom types definition if you are not happy with the way Koson serializes all "primitive" types.
How does it work?
Long story short, everything you use is defined in a single Koson.kt
file.
The obj
is a function with a lambda with receiver parameter that operates over a Koson
type. The Koson class is used to produce an ObjectType
, that can add a finite instance type list to the object.
The arr
is in fact a singleton (object
in Kotlin) that uses the operator overload to permit the use of the []
as a function. It returns an ArrayType
when invoked.
To sum it up, here is a diagram to show the full type hierarchy.
If you are curious and want to check out the implementation details, or why not contribute to improve this library, feel free to check the official github repository here : https://github.com/lectra-tech/koson
If you want to learn more about how Kotlin DSL can be written, you can watch this video (FR only) by Benoit introducing Kotlin DSLs.
There also is a video of a talk I gave (FR only) explaining how a private company can contribute to OSS and why they should, even if they think they don’t have material to do so. It also shortly explains how Koson’s publication process works.
Performance
Now that’s nice, but how does it behave? Is it faster than using your favorite serialization library?
To try answering that question, we’ve built 4 different benchmarks using the JMH OpenJDK tool. If you want to have a look or run the benchmarks on your machine, you’ll find all the details on our github page and in the following embedded project.
To sum it up, here are the results in operations/second (throughput mode), higher = better.
Benchmark |
Score |
Error |
Units |
BigObject - JSON-java |
17120,661 |
± 45,741 |
ops/s |
BigObject - Koson |
17433,982 |
± 372,361 |
ops/s |
BigObject (pretty) - JSON-java |
8902,486 |
± 19,417 |
ops/s |
BigObject (pretty) - Koson |
10252,254 |
± 71,377 |
ops/s |
BigArray - JSON-java |
15272,946 |
± 139,435 |
ops/s |
BigArray - Koson |
14816,130 |
± 132,266 |
ops/s |
BigArray (pretty) - JSON-java |
7744,935 |
± 41,067 |
ops/s |
BigArray (pretty) - Koson |
8607,388 |
± 31,712 |
ops/s |
Testing environment : 3.3 GHz Intel Core i5-6600, 4 cores, VM version: OpenJDK 11.0.1, 64-Bit Server VM, 11.0.1+13
Conclusion
If your only need is having to create JSON
String representations from scratch in Kotlin
, Koson is made for you. Feel free to ⭐ the project if you enjoy it, or why not suggest improvements using issues !
Thanks ;)