Serialize JSON HttpResponse to Kotlin Objects with Ktor.
Recently I had a problem, where I should get the last successful build hash of a Jenkins Job. For this I used a simple json api call via http and got a HttpResponse back. Now, the problem was, that I somehow needed to extract the value for the hash out of this HttpResponse. The first thing that came in mind was just to use substrings, but i recognised that this approach is not that save and future proof, if somebody changes something on the pattern of the json output. I needed a better, yet more programmatically solution to this.
First of all, let’s create the http request with Ktor to get the HttpResponse from Jenkins.
Jenkins API call with Ktor
Required Dependencies
If your Jenkins does not require authentication, you don’t even need the ktor-client-auth dependency.
Defining the HttpClient
This Client uses a basic authentication with the password and username for Jenkins. Don’t forget the part with sendWithoutRequest! If this value isn’t set to true, the authentication will fail every time.
The next part is the JsonFeature with a serializer. It uses the normal kotlinxSerializer for Json. It is recommended to put there the ignoreUnknownKeys value on true, since it will fail, if not every value of the json string can be put into your data classes.
The last step is to configure a ResponseValidator. In my Example, it will throw a custom Jenkins Exception with the response text in it, if the status code is higher than 300.
Serializing Objects to Strings and reversed
Let’s start with a really easy example first and then go to the more difficult ones.
- create serializable data class
@Serializable
data class Data(val a: Int, val b: String)
2. serialize an instance of this class to a string by calling:
Json.encodeToString(Data(42, "str"))
As a result, you get a string containing the state of this object in the JSON format: {"a": 42, "b": "str"}
3. serialize a String to an instance of the class by calling:
val obj = Json.decodeFromString<Data>("""{"a":42, "b": "str"}""")
This will create exactly the same object as defined in step 1.
Creating serializable data classes
Since our Jenkins Json response will look something like this, we need to create classes for everything we want to serialize.
- First of all, we need to create a root class, that contains all the other stuff. In this case I called it JenkinsJsonData.
- The next step what we need to have is the actions. It contains an array of elements in it.
- The last thing we wanna have is the lastBuiltRevision, which contains the SHA1 hash.
Since I also need the buildNumber for an other task later in that project, I’ll add it right away to the parameters in the constructor of the base class.
As you can see, the lastBuiltRevision is nullable. This might be a problem afterwards, if not all of the objects under actions contain a lastBuiltRevision. To make up for that insecurity, we write a custom getter in the JenkinsJsonData class.
It just searches for the last item of these actions, where the lastBuiltRevision is not null and then returns the Hash of it.
Call the Jenkins Api and serialize the output
- Double encode the branchName for the Jenkins Api call.
- call client.get function with the generated URL.
- This function needs to be in a runBlocking block because it is a suspend function.
- This will return a JenkinsJsonData object with the custom getter for the hash.
- If the status code is over 300, the get request will throw an exception and a new custom Exception will be thrown.
Reflection
What went good?
The serialization by itself worked pretty good and the examples on the official documentation were pretty easy to understand and extend. Also, the get Request with Ktor worked pretty good.
What needs improvement?
The reading out of the HttpResponse was causing some trouble, because I had to use the runBlocking block to run it, since it is a suspend function. I didn’t know what such a function was or what it makes special.
Another thing was, that I had some problems with the nullable lastSuccessfulBuild parameter of the actions class. The solution to this problem was really simple and reasonable. The problem was that I didn’t really read the stacktrace carefully enough and that needs definitely improvement.
I hope this article was useful for you 😃