In this post I’m going to illustrate how it’s possible to leverage Ktor to create a shared mobile library that wraps a REST API. The code I’m going to present here can be hosted inside a Kotlin Multi Platform (a.k.a. KMP) project and consumed by any Android and iOS app. For the sake of this post I’m going to target a simple and publicly available API: JSONPlaceholder.

JSONPlaceholder API Overview

The JSONPlaceholder API is rather simple and provides online fake data. Here are the available resources:

Each one of the above resources returns a predefined number of JSON objects containing fake data representing an entity corresponding to the resource name (i.e.: posts, comments, …).

Wrapping the /users resource

To limit the scope of this post I’m going to show how it’s possible to create a wrapper for the /users resource. Such a resource returns the most complex data, which uses nested JSON objects, among the resources provided by JSONPlaceholder. All remaining resources can be wrapped the same way and with less effort since they don’t return nested objects.

Creating the User model

The /users resource returns an array of JSON objects representing fake users. Each user object has the structure shown in the following snippet:

{
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "address": {
        "street": "Kulas Light",
        "suite": "Apt. 556",
        "city": "Gwenborough",
        "zipcode": "92998-3874",
        "geo": {
        "lat": "-37.3159",
        "lng": "81.1496"
        }
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org",
    "company": {
        "name": "Romaguera-Crona",
        "catchPhrase": "Multi-layered client-server neural-net",
        "bs": "harness real-time e-markets"
    }
}

We can take advantage of Kotlin’s @Serializable annotation to create classes to model each user object returned in the JSON. In order to achieve that we are going to need a top level User class and a few nested classes (Address, Company and Geolocation):

@Serializable
data class User(
    val id: Int,
    val name: String,
    val username: String,
    val email: String,
    val address: Address,
    val phone: String,
    val website: String,
    val company: Company
)

@Serializable
data class Address(
    val street: String,
    val suite: String,
    val city: String,
    val zipcode: String,
    val geo: Geolocation
)

@Serializable
data class Geolocation(
    val lat: String,
    val lng: String
)

@Serializable
data class Company(
    val name: String,
    val catchPhrase: String,
    val bs: String
)

Retrieving and parsing JSON data

Now that we have our serializable models in place we will create a class that encapsulates the JSONPlaceholder API functionality. The full code is listed below:

enum class Endpoint(val path: String) {
    Users("/users")
}

class Api() {
    private val baseUrl = "https://jsonplaceholder.typicode.com"
    private val client = HttpClient {
        install(JsonFeature) {
            serializer = KotlinxSerializer()
        }
    }

    suspend fun users(): List<User> {
        return client.get {
            setupCall(Endpoint.Users)
        }
    }

    private fun HttpRequestBuilder.setupCall(endpoint: Endpoint) {
        url {
            takeFrom(urlString = baseUrl)
            encodedPath += endpoint.path
        }
    }
}

The /users resource is modeled using an enum class to encapsulate its details.

Our workhorse is the Api class: It uses Ktor’s default HttpClient which will allows us to make HTTP requests. HttpClient is highly configurable: In this particular case we are instructing it to use KotlinxSerializer to deserialize any HTTP response:

private val client = HttpClient {
    install(JsonFeature) {
        serializer = KotlinxSerializer()
    }
}

NOTE

KotlinxSerializer uses strict mode parsing by default. This means that the deserialization will fail if it encounters unknown or malformed keys when parsing the JSON payload. In our scenario, this is not an issue because our models are built to capture all the properties of the JSON object they represent.

The default strict mode parsing behavior can be turned off by explicitly setting the serializer configuration. In earlier versions of KotlinxSerializer, the strict mode parsing behavior was determined by a single constructor argument: strictMode. The current version of KotlinxSerializer, instead, provides three constructor arguments that allow to relax the parsing requirements:

The default strict mode parsing behavior can be turned off by explicitly setting the serializer configuration as follows:

private val client = HttpClient {
    install(JsonFeature) {
        serializer = KotlinxSerializer(
            Json(
                JsonConfiguration(
                    ignoreUnknownKeys = true,
                    isLenient = true,
                    serializeSpecialFloatingPointValues = true
                )
            )
        )
    }
}

The setupCall helper method allows us to easily build an url instance given the desired endpoint:

private fun HttpRequestBuilder.setupCall(endpoint: Endpoint) {
    url {
        takeFrom(urlString = baseUrl)
        encodedPath += endpoint.path
    }
}

The created url instance will be used by Ktor to make an HTTP call:

suspend fun users(): List<User> {
    return client.get {
        setupCall(Endpoint.Users)
    }
}

Ktor’s HttpClient will infer User as the class for deserializing the HTTP response and will try to parse the JSON response and return the result as a List<User> instance.

Each remaining JSONPlaceholder resource (i.e.: posts, comments, …) can be wrapped using the same approach illustrated above.

Conclusion

You can find the full code for wrapping JSONPlaceholder here.

In this post we examined how it’s possible to leverage Ktor to nicely wrap a REST API. In particular we saw how we can use Ktor’s HttpClient to make HTTP requests and KotlinxSerializer to deserialize HTTP responses into Kotlin serializable classes.

In the next post we’re going to take a look at how we can create unit tests for HTTP calls.